Dynamic Rose Tab

Located at dynamic_analyzer/tabs/dynamic_rose_tab.py, the Rose Tab (implemented as the mixin class RoseTab) is one of six coordinated views inside the FracSTAT Dynamic Analyzer workspace (Map, Rose, Mohr, Stereonet, Table, and Method). Its function is to render an equal-area, strike-frequency rose diagram of the fracture population currently loaded into the workspace, with each angular petal coloured according to the resolved stress attribute selected elsewhere in the application (slip tendency, dilation tendency, fracture susceptibility, or a four-tier critical-fracture-state classification).

Unlike the Static Analyzer's Rose Plot tab, which characterises fracture-network geometry alone (see static_rose_tab.py), the Dynamic Analyzer's Rose Tab is a stress-resolved diagram: it fuses the purely geometric distribution of strike orientations with the mechanical behaviour of each fracture under a user-specified in-situ stress tensor. This dual encoding — petal length for frequency (or cumulative length) and petal colour for mechanical response — allows an analyst to identify, at a glance, which strike orientations are both abundant in the dataset and mechanically favourable for slip, dilation, or Coulomb failure under the current stress field.

The module operates on the same segment population used throughout the Dynamic Analyzer: either straight-line segments digitised from an SVG trace map (with stresses resolved via calculator.py) or fracture-plane records imported from Excel/CSV (legacy Strike/Dip tables, Trend/Plunge pole tables, or focal-mechanism nodal-plane tables). The Rose Tab consumes the already-computed self.results DataFrame; it performs no independent stress calculation of its own.

1. Purpose and Scope

1.1 Position within the FracSTAT Architecture

The class RoseTab is combined with MapTab, MohrTab, TableTab, MethodTab, and StereonetTab through Python multiple inheritance into the top-level SlipTendencyApp window (dynamic_app.py). RoseTab therefore assumes that the host object exposes a defined contract of shared attributes and helper methods, listed below.

Shared attribute / methodOriginRole for the Rose Tab
self.tabsSlipTendencyApp._build_layoutCTkTabview hosting the "Rose" pane into which the tab's widgets are placed.
self.resultscalculator.run_calculation()Per-segment DataFrame of geometry, orientation, and resolved-stress columns; the sole data source for the diagram.
self.current_stressSlipTendencyApp.calculate()Dict of the active stress-tensor parameters.
self.plot_palette()SlipTendencyAppReturns a dict of theme colours (background, text, muted, spine, etc.) used for the empty-state message.
self._selected_attribute()SlipTendencyAppResolves the user's chosen colour-by metric (Map Display dropdown) to a (column, axis label, title fragment) triple.
self.attribute_colormap_and_norm()SlipTendencyAppReturns the (colormap, Normalize/BoundaryNorm, is_critical) triple used consistently by the Map, Rose, and Mohr tabs.
self.save_plot_image()SlipTendencyAppSerialises the current Matplotlib Figure to a PNG in the project's export directory.

1.2 Intended Audience

This guide is written for structural geologists, reservoir geomechanics engineers, and rock-mechanics researchers who use FracSTAT to evaluate the reactivation potential of natural fracture networks under an assumed or measured in-situ stress state. Familiarity with basic stress-tensor concepts (principal stresses, Mohr-Coulomb failure, Andersonian faulting theory) is assumed; the relevant formulae are nonetheless reproduced in Section 5 for reference.

2. Interface Overview

The Rose Tab occupies the full client area of the "Rose" pane in the Dynamic Analyzer's main CTkTabview. Its layout is a two-column CTkFrame grid: a fixed-width plot card on the left (column 0, expandable) containing the Matplotlib canvas, and a fixed-width, vertically scrollable settings panel on the right (column 1, 290 px).

Overall layout of the Dynamic Rose Tab showing the polar rose diagram and the settings sidebar
Figure 2.1 — Overall layout of the Rose Tab: polar rose diagram (left) with toolbar, and the scrollable settings panel (right) topped by the fracture-state classification table and grouped below it into five labelled cards.

2.1 Plot Card

  • Toolbar label — a muted caption reading "Equal-area Rose Plot" identifying the diagram type.
  • Save Image button — invokes self.save_plot_image("rose", self.rose_fig), writing a high-resolution PNG (DPI controlled by the application-wide export setting) to the project's FracSTAT_plot_exports folder.
  • Polar Matplotlib canvas — a single polar Axes (self.rose_ax) embedded via FigureCanvasTkAgg, on which the rose petals, reference circles, compass labels, and title are drawn.
  • Colour-bar axes — a dedicated, independently positioned Axes (self.rose_cbar_ax) reserved for the colour scale legend; its rectangle is recomputed on every redraw according to the chosen colour-bar position.

2.2 Settings Panel

The settings panel is a CTkScrollableFrame topped by a fixed fracture-state classification table (Section 2.2.1) and subdivided below it into five visually separated cards, each with a bold section header and a thin horizontal rule. Every control is wired directly to self._draw_rose(), so changes are reflected in the plot immediately (no separate "Apply" step), with the exception of colour-swatch pickers, which redraw as soon as a colour is confirmed in the picker dialog.

CardControls contained
Data & BinsBins, Weight by, Angle unit, Custom title, Half rose toggle
PetalsPetal gap fraction, Petal edge colour
Reference CirclesShow circles, show angular labels, show compass, show radial grid, circle line width, circle colour
Background ColoursOuter (figure) background, inner (axes) background, boundary colour, boundary line width
Colour BarPosition, length fraction, show plot title toggle

A "Reset to Defaults" button is docked at the bottom of the settings panel and restores every control listed above to its factory value in a single action (see Section 4.7 for the precise reset values, which differ subtly from the tab's initial construction defaults). The classification table is not a user-configurable control and is unaffected by Reset to Defaults; it simply reflects whatever data is currently loaded.

2.2.1 Fracture-State Classification Table

Docked at the very top of the settings panel, above the five cards described in Section 2.2, is a fixed four-row summary table (built by _build_rose_classification_table()) with columns "Classification state", "Fractures (percentage)", and "Dominant trends". Unlike the rest of the settings panel, this table is not a set of input controls — it is a read-only summary that is refreshed on every redraw (_update_rose_classification_table(), called from _draw_rose() with the diagram's current bin count) and is always visible regardless of which Map Display metric is currently selected for petal colouring.

For each of the four states — Stable, Near Critical, Critical, Unstable, colour-coded to match the palette used elsewhere in the app — the table reports:

  • Fractures (percentage) — the count and percentage of all loaded segments (regardless of orientation) whose CriticalState string column equals that state, e.g. "34.2% (128)".
  • Dominant trends — among just the segments in that state, the strikes are folded to 0–180° and binned into the same number of angular bins as the current rose diagram; the modal bin's centre and its axial conjugate (offset by 180°) are reported as a "NNN° / NNN°" pair. This identifies which strike orientation is most represented within that risk tier, independent of the strike distribution of the population as a whole. Empty states display "–" in both columns.

Because it is always driven by CriticalState irrespective of the active colouring metric, this table lets an analyst check the dataset's overall risk profile (e.g. "12% of fractures are Critical or Unstable, concentrated around 040°/220°") even while the rose diagram itself is coloured by, say, Slip tendency or Fracture susceptibility.

Native Rose controls showing 36 bins, Length weighting, and the fracture-state summary
Figure 2.2 — Rose controls and fracture-state summary for the existing SVG dataset. With 36 bins and Length weighting, the table reports 184 Stable segments (9.1%) and 1,839 Near Critical segments (90.9%); Critical and Unstable counts are zero. The table summarises segment states independently of the slip-tendency colours on the petals. Angular labels are enabled and duplicate compass labels disabled for readability.

3. Data Input

The Rose Tab does not load, parse, or import any files itself. It is strictly a consumer of self.results, the DataFrame produced by core/calculator.py after a successful Calculate action in the sidebar, or refreshed automatically whenever the stress parameters, geometry, or dip overrides change. The columns the Rose Tab actually reads are summarised below.

ColumnTypeUsed for
StrikeDegfloat, 0–360°Base orientation of each segment/plane; folded to 0–180° before binning.
Length_mfloatOptional length weighting when Weight by = Length.
<attribute column>float or boolWhichever column self._selected_attribute() resolves to (SlipTendency[Norm], DilationTendency, FractureSusceptibility[Norm], or CriticalStateValue); used to colour each petal.

3.1 Data Provenance and Column Semantics

StrikeDeg is stored in self.results as a fully-directed 0–360° value (see models.py's wrap_360 helper) rather than pre-folded to the 0–180° fracture-orientation convention. This design choice is deliberate: preserving the full compass direction lets the Stereonet and Mohr tabs correctly place a fracture pole on the correct hemisphere once a per-segment dip override changes the plane from vertical to inclined. The Rose Tab performs its own fold (StrikeDeg % 180.0) at draw time, because a rose diagram is inherently an axial (undirected-line) statistic: a joint striking 045° and one striking 225° describe the same physical orientation and must contribute to the same petal.

Length_m originates from two different geometric pathways depending on the data source. For SVG-derived segments it is the true, scale-calibrated Euclidean segment length. For Excel-sourced data without genuine spatial coordinates (legacy fracture-plane tables, Trend/Plunge pole tables, or focal-mechanism nodal planes) Length_m is populated with zero for every row unless the source spreadsheet happened to supply an explicit Length column; see Section 3.2 for how the Rose Tab compensates for this.

3.2 Handling of Missing or Degenerate Length Data

When Weight by = Length is selected but the Length_m column is entirely absent, all-zero, non-finite, or negative, the module falls back transparently to an equal weight of 1.0 per segment — identical to the Static Analyzer's own Rose Plot tab fallback behaviour (see the code comment preserved in dynamic_rose_tab.py referencing static_angles.py). This prevents the diagram from silently rendering as blank when Excel-sourced orientation-only data (no true geometric length) is loaded, while still weighting by true length whenever that information genuinely exists.

3.3 Empty-State Handling

If self.results is an empty DataFrame (no segments loaded, or the most recent Calculate action failed validation), the Rose Tab clears both axes and colour-bar axes and renders the single caption "No data" in the workspace's muted text colour, then exits without attempting any further computation. No exception is raised and the settings panel remains fully interactive so a user can pre-configure display options before data is loaded.

4. Settings and Controls, Card by Card

This section documents every interactive control exposed in the settings panel, in the order in which it appears, together with its underlying ui_common.qtk variable name, default value, valid range, and the precise effect on the redraw routine (_draw_rose).

4.1 Data & Bins Card

ControlVariableType / RangeDefaultEffect
Binsrose_bins_varEntry (default 36, range 4–360)36Number of equal-width angular bins spanning the 0–180° (half rose) or 0–360° (full rose) range. Clamped internally to [4, 360] even if a value were entered programmatically outside the dropdown list.
Weight byrose_weight_varDropdown: Length, Count, EqualLengthSelects the per-segment weight fed into the weighted histogram (see Section 6.2).
Angle unitrose_angle_unit_varDropdown: Degrees, Radians, GradiansDegreesControls the numeric format of the angular tick labels only; the underlying binning is always performed in radians and is unaffected.
Custom title (blank = auto)rose_title_varFree text"" (empty)When non-blank and Show plot title is enabled, overrides the automatically generated title string.
Half rose (0–180° only)rose_half_rose_varCheckboxOffRestricts the diagram to a semicircular 0–180° wedge instead of mirroring every strike into both 0–180° and 180–360°.

4.2 Petals Card

ControlVariableRangeDefaultEffect
Gap between petals (0–1)rose_petal_gap_var0.1–1.00.92Fraction of the full bin angular width actually drawn as the petal's bar width; the remainder appears as whitespace between adjacent petals. A value of 1.0 produces edge-to-edge petals; smaller values increase visual separation.
Petal edge colourrose_petal_edge_color_varHex colour#FFFFFFStroke colour applied to every petal's outline (linewidth fixed at 0.6 pt in the drawing routine).

4.3 Reference Circles Card

ControlVariableTypeDefaultEffect
Show reference circlesrose_show_circles_varCheckboxOnToggles visibility of the polar axes' outer boundary spine (labelled "circles" in the UI, technically the ax.spines['polar'] outline).
Show angular labelsrose_show_labels_varCheckboxOffToggles the numeric/compass tick labels around the circumference.
Show compass (N/E/S/W)rose_show_compass_varCheckboxOnDraws bold N/E/S/W text annotations just outside the outermost petal radius; only rendered when the diagram is a full (not half) rose.
Show radial gridrose_show_grid_varCheckboxOffToggles both the angular and radial (y-axis) gridlines inside the polar plot.
Circle line widthrose_circle_lw_varNumeric text, pt0.8Line width applied to the radial/angular grid lines when Show radial grid is enabled.
Circle colourrose_circle_color_varHex colour#D9E2ECColour applied to the radial/angular grid lines.
Note: "Show reference circles" governs the single bold outer boundary (the polar spine), whereas "Show radial grid" governs the internal concentric/radial gridlines. Both draw circular geometry but are independent toggles with independent colour/width sources — the boundary always uses rose_boundary_color_var / rose_boundary_width_var (Background Colours card), while the internal grid always uses rose_circle_color_var / rose_circle_lw_var (this card).

4.4 Background Colours Card

ControlVariableTypeDefaultEffect
Outer (figure) backgroundrose_outer_bg_varHex colour#F5F7FAFills the Matplotlib Figure background, i.e. the area outside the circular plot but inside the plot card.
Inner (rose) backgroundrose_inner_bg_varHex colour#FFFFFFFills the polar Axes background (the disc enclosed by the outer boundary).
Boundary colourrose_boundary_color_varHex colour#1F2933Colour of the polar spine (outer circular boundary), when "Show reference circles" is enabled.
Boundary line widthrose_boundary_width_varNumeric text, pt1.5Width of the outer boundary spine.

4.5 Colour Bar Card

ControlVariableType / RangeDefaultEffect
Positionrose_colorbar_pos_varDropdown: Bottom, Top, Left, Right, NoneBottomSelects which side of the plot the colour-bar legend is docked to, or hides it entirely (None).
Length (e.g., 0.72 for 72%)rose_colorbar_length_varNumeric text, 0.1–1.00.72Fractional length of the colour bar relative to the available figure dimension along its long axis.
Show plot titlerose_show_title_varCheckboxOnToggles rendering of the diagram's title text (auto-generated or custom, per Section 4.1).

4.6 Colour-by Metric (Controlled Elsewhere)

The Rose Tab does not itself expose a colour-by dropdown; the metric is shared application-wide with the Map tab via the sidebar's "Map Display" selector (self.map_mode_var), further modulated by the "Colour by normalized Tₛ" checkbox (self.normalized_var). The four selectable metrics and their resolved DataFrame columns are listed below.

Map Display selectionNormalised?Resolved columnColour-bar label
Slip tendencyYes (default)SlipTendencyNormNormalized slip tendency Tₛ
Slip tendencyNoSlipTendencySlip tendency Tₛ
Dilation tendencyn/aDilationTendencyDilation tendency Td
Fracture susceptibilityYesFractureSusceptibilityNormNormalized fracture susceptibility (inverted — 1 = least stable; see Section 5.5)
Fracture susceptibilityNoFractureSusceptibilityFracture susceptibility Sf, MPa
Critical fracturesn/aCriticalStateValueCritical fracture state: 0=Stable, 1=Near Critical, 2=Critical, 3=Unstable

4.7 Reset to Defaults

The Reset to Defaults button invokes self._rose_reset_defaults(), which writes a fixed set of values into every control and then calls self._draw_rose(). Two values differ subtly from the construction-time defaults set by self._rose_init_vars(): Show angular labels resets to On (constructed default: Off) and Show radial grid resets to On (constructed default: Off). All other values are identical between initial construction and reset.

5. Mathematical Background

This section defines, with explicit notation, every quantity computed or displayed by the Rose Tab. Variable names in the source code are given alongside standard geological/statistical symbols where they differ.

5.1 Strike Folding (Axial Data Convention)

A fracture strike is an axial quantity: strikes θ and θ+180° describe the identical line orientation, so the appropriate statistical treatment is that of axial rather than directional data (Mardia and Jupp, 2000). The module therefore folds every strike into the canonical 0–180° domain prior to binning:

θ_fold = θ_raw mod 180°

where θ_raw is the StrikeDeg column value as stored (0–360°, fully directed). When the diagram is drawn in full-rose mode (Half rose unchecked), the folded population is mirrored by adding 180° to obtain a second, duplicate copy, so that the resulting circular histogram visually fills all four quadrants while still representing purely axial statistics — the diagram is symmetric about the origin by construction, not because of any new physical information.

5.2 Weighted Angular Histogram

Let φᵢ be the strike (in radians, after folding and optional mirroring) of segment i, and wᵢ its associated weight (Section 6.2). For a chosen bin count n, the angular domain [0, Φ] — with Φ = π for a half rose or 2π for a full rose — is partitioned into n equal-width bins with edges

e_k = k·Φ/n, k = 0, 1, ..., n

The weighted count assigned to bin k is

C_k = Σ w_i, for all i such that e_k ≤ φ_i < e_(k+1)

This is computed via a single call to numpy.histogram with the weights argument set to the per-segment weight vector (Harris et al., 2020), which is the standard, numerically efficient implementation of a weighted circular histogram.

5.3 Equal-Area Petal Radius Transformation

A conventional rose diagram that plots petal radius directly proportional to bin frequency visually over-emphasises the dominant orientation, because the area of a circular sector grows with the square of its radius. FracSTAT follows the standard structural-geology convention of an equal-area (square-root) rose diagram (Fisher, 1993; Davis et al., 2011), in which the plotted radius r_k of each petal is the square root of its weighted count:

r_k = √C_k

Under this transformation, the area of petal k (approximately (1/2)·r_k²·Δθ for narrow bins) becomes directly proportional to C_k, so equal areas on the page correspond to equal frequencies (or equal cumulative lengths, if length-weighted), giving a perceptually honest representation of the underlying distribution and matching the convention used by the Static Analyzer's Rose Plot tab and by widely used structural-geology software.

5.4 Colour-Mapped Attribute Aggregation

Each petal's fill colour represents the mean value of the selected mechanical attribute A (one of Tₛ (Morris et al., 1996), Td (Ferrill and Morris, 2003), Sf (Streit and Hillis, 2004), or the four-tier critical-fracture-state value) among all segments falling in that bin:

Ā_k = nanmean{ A_i : e_k ≤ φ_i < e_(k+1) }

If a bin contains no segments, Ā_k defaults to 0.0 rather than NaN, which prevents matplotlib colour-mapping errors for empty angular sectors. The colour scale and normalisation strategy applied to Ā_k are delegated to self.attribute_colormap_and_norm(), the same routine used by the Map and Mohr tabs, ensuring colour consistency across the whole Dynamic Analyzer workspace (see Section 5.5).

5.5 Normalisation Strategy

Continuous attributes (SlipTendency, SlipTendencyNorm, DilationTendency, FractureSusceptibility, FractureSusceptibilityNorm) are mapped through a standard linear Normalize instance. Columns whose name ends in "Norm", together with DilationTendency, are fixed to the domain [0, 1]; all other continuous columns use the finite data-driven minimum and maximum, with a small symmetric padding applied when the data range collapses to a single value (to avoid a degenerate colour scale). Note that FractureSusceptibilityNorm is inverted at source in core/calculator.py — 1 − (S_f − min) / (max − min) — because a lower S_f means a smaller remaining pore-pressure margin; on this tab that means a petal at the top of the [0, 1] colour domain contains the least stable fractures, and the scale is relative to the loaded dataset rather than to a failure threshold. The CriticalStateValue attribute instead uses a discrete four-colour colormap/norm banding its integer codes 0–3 (Stable, Near Critical, Critical, Unstable), guaranteeing that each state maps unambiguously to its own flat colour swatch rather than a continuous gradient, and the colour bar in this special case is manually re-ticked to read "Stable" / "Near Critical" / "Critical" / "Unstable" instead of numeric values. Because each petal's colour value Ā_k is a mean of the per-segment state codes in that bin (Section 5.4), a petal spanning a mix of states can render at a boundary or blended position on the discrete scale rather than falling cleanly into a single band.

6. Algorithms

6.1 Bin Count Resolution and Sanitisation

The bin-count text is parsed with a defensive try/except around int(); a parse failure falls back silently to 36. The resulting integer is clamped to the closed interval [4, 360] via max(4, min(360, n)), guarding against pathological inputs (e.g., a bin count of 0 that would crash numpy.histogram, or an excessively large value that would render an unreadably dense diagram).

6.2 Weight Vector Construction

ModeWeight constructionFallback condition
Lengthw_i = Length_m_i (metres, or the active display unit)If Length_m is absent, or every value is non-finite/≤0, w_i = 1.0 for all i (equal weighting).
Countw_i = 1.0None — always uniform.
Equalw_i = 1.0None — identical numerical result to Count in the current implementation.

6.3 Mirroring for Full-Rose Display

When Half rose is unchecked, the strike array and its weight array are each duplicated: the mirrored strike array is np.concatenate([strikes, strikes + 180]), and the mirrored weight array is np.tile(weights, 2). The colour-attribute array is likewise duplicated with np.tile so that every mirrored strike sample still carries its correct source attribute value into the per-bin colour-averaging step.

6.4 Petal Geometry Assembly

  1. Compute bin edges over [0, Φ] for n bins; derive bin centres and widths.
  2. Compute the weighted histogram C_k over the (possibly mirrored) strike population.
  3. Compute equal-area radii r_k = √C_k.
  4. For each bin, gather the subset of attribute values whose strike falls in [e_k, e_(k+1)) and take the mean (0.0 if empty) to obtain the per-petal colour value Ā_k.
  5. Resolve (colormap, norm, is_critical) from self.attribute_colormap_and_norm(Ā, value_column).
  6. Map Ā through a ScalarMappable to obtain an RGBA colour array, one entry per bin.
  7. Draw all bins in a single ax.bar(...) call with theta = bin centres, height = r_k, width = (bin width) × petal-gap fraction, and per-bin RGBA facecolor.
python
strikes_deg = self.results["StrikeDeg"].to_numpy(dtype=float) % 180.0
if half:
    strikes_full = strikes_deg
    weights = raw_weights
else:
    strikes_full = np.concatenate([strikes_deg, strikes_deg + 180.0])
    weights = np.tile(raw_weights, 2)

strikes = np.deg2rad(strikes_full)
bins = np.linspace(0.0, full_range, num_bins + 1)
counts, edges = np.histogram(strikes, bins=bins, weights=weights)
centers = (edges[:-1] + edges[1:]) / 2.0
widths = np.diff(edges)

# per-bin mean attribute value -> petal colour
color_values = []
for low, high in zip(edges[:-1], edges[1:]):
    mask = (strikes >= low) & (strikes < high)
    color_values.append(
        float(np.nanmean(attribute_values[mask])) if mask.any() else 0.0
    )
color_values = np.array(color_values)

Simplified excerpt of _draw_rose() showing the strike-folding, mirroring, weighted-histogram, and per-bin colour-aggregation steps.

6.5 Angular Tick-Label Generation

Tick positions are always generated at a fixed 10° cadence in the underlying degree domain (np.arange(0, 360, 10)), regardless of the display unit chosen. The three supported unit modes reformat the same tick positions rather than recomputing them:

Unit modeTick spacing used for labelsLabel format
Degrees10° (0, 10, 20, …, 350)"{n}°"
Radians30° (0, 30, …, 330)Multiples of π expressed as a decimal fraction, e.g. "0.33π"; the origin is rendered as "0"
Gradians10° (0, 10, …, 350)Degrees converted via g = d × 400/360 and rendered as "{g}g"

6.6 Colour-Bar Layout Algorithm

Because the colour bar can be docked to any of four sides (or hidden), the module maintains four hard-coded layout presets, each specifying (a) the [x0, y0, width, height] rectangle (figure-fraction coordinates) for the dedicated colour-bar Axes, and (b) a corresponding rectangle that the polar Axes itself is resized/repositioned into so the two never overlap. The presets are summarised below (all values are matplotlib figure-fraction coordinates, i.e. 0–1).

PositionColour-bar rectangle [x0, y0, w, h]Polar-axes rectangle [x0, y0, w, h]Orientation
Bottom[(1−L)/2, 0.06, L, 0.030][0.15, 0.20, 0.70, 0.65]Horizontal
Top[(1−L)/2, 0.92, L, 0.030][0.15, 0.10, 0.70, 0.65]Horizontal
Left[0.03, (1−L)/2, 0.025, L][0.18, 0.15, 0.70, 0.70]Vertical
Right[0.93, (1−L)/2, 0.025, L][0.10, 0.15, 0.70, 0.70]Vertical
None(hidden)[0.15, 0.12, 0.70, 0.75]n/a

L denotes the user-supplied colour-bar length fraction (rose_colorbar_length_var), clamped to [0.1, 1.0]. This deterministic, closed-form layout avoids the layout-engine recomputation cost of matplotlib's constrained_layout on every redraw and guarantees pixel-stable behaviour when a user resizes the colour bar interactively.

7. Internal Workflow

This section summarises the end-to-end control flow triggered whenever the Rose Tab must be (re)drawn — on initial tab construction, whenever a setting changes, whenever new stress results are calculated, and whenever a fracture segment is selected/deselected elsewhere in the workspace.

7.1 Construction Sequence

  1. SlipTendencyApp._build_layout() calls self._build_rose_tab() once, during workspace initialisation.
  2. _build_rose_tab() calls self._rose_init_vars() to lazily create every CTk variable (using hasattr guards so repeated calls are idempotent).
  3. The plot card, toolbar, Matplotlib Figure/Axes/canvas, and dedicated colour-bar Axes are instantiated and gridded.
  4. self._build_rose_settings_panel(tab) constructs the five-card scrollable settings panel and wires every widget's command/binding to self._draw_rose (or, for text-entry fields, to the widget's <Return> and <FocusOut> events).
  5. No explicit initial draw call occurs inside _build_rose_tab(); the first paint happens the moment the shared refresh_plots()/calculate() pipeline (in dynamic_app.py) subsequently invokes self._draw_rose() once data becomes available, or via the workspace's periodic full-refresh path.

7.2 Redraw Sequence (_draw_rose)

  1. Guard clause: if self.rose_ax has not yet been created, return immediately (defensive check for calls that may race construction).
  2. Read the background, edge, and circle colour values from their StringVars.
  3. Clear both the polar Axes and the colour-bar Axes; hide the colour-bar Axes pending recomputation; apply the outer and inner background colours to the Figure and Axes respectively.
  4. If self.results is empty, render the "No data" caption and return.
  5. Parse and clamp the bin count.
  6. Resolve the half/full-rose flag and the corresponding angular domain Φ.
  7. Build the weight vector per Section 6.2.
  8. Fold StrikeDeg into 0–180°; mirror strikes/weights into 0–360° if a full rose is requested.
  9. Compute the weighted histogram and equal-area radii.
  10. Resolve the active colour-by attribute via self._selected_attribute(); tile the attribute array to match any mirroring; compute per-bin mean attribute values.
  11. Resolve (cmap, norm, is_critical) via self.attribute_colormap_and_norm() and map to an RGBA array.
  12. Parse and clamp the petal-gap fraction; draw all petals in one ax.bar() call.
  13. Configure polar orientation (theta zero at North, clockwise direction, half/full theta limits).
  14. Parse the circle line width; compute and apply angular tick positions/labels per the selected unit, or clear tick labels if disabled.
  15. Apply boundary spine visibility/colour/width; apply internal grid visibility/colour/width; hide radial (r-axis) tick labels unconditionally.
  16. If compass labels are enabled and the rose is a full rose, annotate N/E/S/W at 1.10× the maximum petal radius.
  17. If the title is enabled, compute the auto title string (or use the custom override) and apply it, sized from the current global font-size rcParam.
  18. Parse and clamp the colour-bar length; select and apply the layout preset from Section 6.6, or hide the colour bar and expand the polar Axes to fill the card if Position = None.
  19. Call self.rose_canvas.draw_idle() to schedule an asynchronous repaint.

7.3 Cross-Tab Interactions

The Rose Tab is a passive consumer within the broader workspace: it exposes no segment-selection or picking behaviour of its own (unlike the Map, Mohr, and Stereonet tabs, which support click-to-select and cross-tab highlighting). It is, however, refreshed automatically whenever refresh_plots() is invoked following a Calculate action, a colour-scale change, a stress-parameter edit, a dip-override load, or a global font/appearance change (via _apply_workspace_font, which forces a redraw of every plot so that title and label font sizes stay in sync with the header's Font Size control).

8. Statistical Methods

8.1 Circular (Directional) Statistics Context

The rose diagram is fundamentally a graphical circular-statistics device for axial data (Mardia and Jupp, 2000; Fisher, 1993), and is standard structural-geology practice (Davis et al., 2011). Because fracture strikes are axial (mod 180°) rather than directional (mod 360°) quantities, the folding and mirroring steps described in Section 5.1 are essential; failing to fold strikes before binning would artificially split a genuinely unimodal fracture set into two half-populations 180° apart.

8.2 Weighted Frequency vs. Length-Weighted Intensity

Selecting Weight by = Length converts the diagram from a simple strike-frequency histogram (each fracture contributes equally regardless of size) into a length-weighted orientation histogram, which more closely approximates the areal or volumetric fracture intensity contributed by each strike set — a metric of direct relevance to fluid-flow anisotropy and fracture-network connectivity assessments. Selecting Count or Equal instead answers a purely frequency-based question ("how many fractures strike in this direction"), which is less sensitive to a small number of unusually long fractures dominating the diagram.

8.3 Per-Bin Attribute Aggregation as a Simple Statistical Estimator

The colour value assigned to each petal, Ā_k (Section 5.4), is an unweighted arithmetic mean of the selected mechanical attribute over all segments in that bin — it is not weighted by the same length/count weighting used for the petal radius. Analysts should therefore interpret petal colour as "the typical stress-attribute value among fractures at this orientation" and petal size as "how much of the population (by count or length) occurs at this orientation"; the two encodings answer distinct statistical questions and are not meant to be multiplied together to recover a single combined score.

8.4 Handling of Empty Bins

Bins containing zero segments are assigned Ā_k = 0.0, and will typically render as either the coolest end of the active colormap or, for the critical-fracture-state metric, the "Stable" colour swatch. Users comparing colour intensity across bins with very different populations (e.g., a dominant fracture set with hundreds of members versus a minor set with two or three) should cross-reference petal length before drawing mechanical conclusions from colour alone, since a small-n bin's mean attribute value is a comparatively unstable estimator.

9. Visualization Components

9.1 Polar Coordinate Convention

The Axes is configured with theta_zero_location = 'N' (0° at the top of the plot, matching geographic North) and theta_direction = -1 (clockwise increasing angle), reproducing the standard geological compass-bearing convention rather than matplotlib's mathematical default (0° at east, counter-clockwise).

9.2 Petal Rendering

All petals are drawn with a single vectorised ax.bar(theta, radius, width, bottom=0, color=..., edgecolor=..., linewidth=0.6) call rather than one bar() call per bin, which is significantly faster for large bin counts and produces a single collection of matplotlib artists that is easier to manage across repeated redraws.

9.3 Reference Geometry

  • Outer boundary spine — the polar plot's circular frame, colour/width controlled by the Background Colours card, visibility by the Reference Circles card's "Show reference circles" toggle.
  • Internal angular/radial grid — matplotlib's native polar grid, colour/width from the Reference Circles card, visibility from "Show radial grid".
  • Compass annotations — bold N/E/S/W text placed at 1.10× the maximum bar radius, shown only for full (not half) roses.

9.4 Colour Bar

The colour bar is a standard matplotlib Colorbar object bound to the ScalarMappable created from the resolved colormap and norm. For the CriticalStateValue metric, its ticks are explicitly overridden to the four labelled positions [0, 1, 2, 3] with text labels "Stable", "Near Critical", "Critical", and "Unstable" rather than a numeric scale, which is essential because the underlying discrete norm would otherwise present as a continuous-looking gradient despite representing a fixed four-state classification.

9.5 Title

When enabled, the title reads either the user-supplied custom string, or the automatically generated string "Equal-area rose coloured by {value_title}", where value_title is the third element of the tuple returned by self._selected_attribute() (e.g., "Slip tendency", "Dilation tendency"). Title font size is drawn from matplotlib's live axes.titlesize rcParam, which is itself kept synchronised with the workspace header's Font Size control by _apply_workspace_font, so all diagrams across the Dynamic Analyzer scale their text together.

Half-rose diagram length-weighted, coloured by fracture susceptibility, with radial grid and angular labels enabled
Figure 9.2 — Half-rose diagram (0–180°) length-weighted, coloured by fracture susceptibility, with radial grid labels enabled.

10. Advanced Features

10.1 Synchronised Colour Semantics Across Tabs

Because self.attribute_colormap_and_norm() and self._selected_attribute() are shared, application-level methods rather than Rose-Tab-local logic, changing the Map Display metric or the active colormap in the sidebar simultaneously updates the Map tab, the Rose tab, and (for the continuous metrics) the Mohr tab's colour field, guaranteeing that a given colour always represents the same numeric value everywhere in the workspace during a single analysis session.

10.2 Unit-Agnostic Length Weighting

The Length_m column already reflects the workspace's active display unit conversion pipeline (the profile width/height and unit selector in the sidebar), so switching the display unit (m, cm, mm, km) does not require any additional handling inside the Rose Tab — the weighted histogram automatically reflects lengths in whichever unit is currently active, though the colour-bar and axis labels for the rose diagram itself carry no unit text since the y-axis (radius) has no numeric meaning beyond relative area.

10.3 Interactive Colour Pickers

Every colour field in the settings panel uses the shared _StaticColorButton widget (ui_widgets.py), which opens a 40-swatch preset palette (5 shade rows × 8 hues) plus a free-form hex-code entry with a live preview swatch. Selecting or confirming a colour immediately triggers self._draw_rose() via the widget's callback, and the corresponding StringVar is kept in sync via a trace_add('write', ...) binding so the swatch button visually reflects the underlying variable's current value even if it is changed programmatically (e.g., by Reset to Defaults).

10.4 Dynamic Font Scaling

Font sizes for the title, tick labels, and colour-bar text are never hard-coded; they are read live from matplotlib's global rcParams (axes.titlesize, xtick.labelsize) at draw time, which are themselves driven by the workspace header's Font, Font Size, Bold, and Colour controls. This design keeps every plot in the Dynamic Analyzer — Map, Rose, Mohr, Stereonet — visually consistent without requiring the Rose Tab to know about the header's implementation.

11. Performance Optimization

TechniqueWhere appliedBenefit
Vectorised NumPy histogramSection 6.2/6.4, np.histogram(weights=...)O(n) weighted binning without an explicit Python loop over segments.
Single ax.bar() call for all petalsSection 9.2Avoids per-bin matplotlib artist creation overhead; scales to the maximum supported 360 bins without perceptible lag.
draw_idle() rather than draw()End of _draw_roseDefers the actual canvas repaint to Qt's event loop, coalescing rapid successive setting changes (e.g., a slider or rapid typing) into fewer physical repaints.
Deterministic closed-form colour-bar layoutSection 6.6Avoids the per-frame cost of matplotlib's automatic constrained/tight layout solvers.
Idempotent variable initialisationSection 7.1, hasattr guards in _rose_init_varsPrevents redundant variable churn if the tab-build path is ever invoked more than once.
Shared colormap/normalisation resolutionSection 10.1Colour-scale computation logic lives in one place (SlipTendencyApp) rather than being duplicated per tab, reducing both code size and the chance of visual inconsistency.

No caching layer is applied to the Rose Tab's own computation, because the weighted-histogram and colour-aggregation steps are computationally inexpensive (typically well under a few milliseconds for datasets of several thousand segments) relative to the cost of a full stress recalculation (core/calculator.py) or a Matplotlib canvas repaint; the module instead relies on the shared workspace-level dirty-tracking (self._ws_mark_dirty()) and on-demand redraw pattern common to all Dynamic Analyzer tabs.

12. Practical Examples

12.1 Example 1 — Identifying the Dominant, Critically-Stressed Fracture Set

  1. Load an SVG trace map or Excel orientation table and click Calculate with a representative in-situ stress tensor.
  2. In the sidebar, set Map Display to "Critical fractures".
  3. Switch to the Rose tab; set Weight by = Length and Bins = 36 for a balanced angular resolution.
  4. Observe which petal(s) are both large (abundant/long fractures at that strike) and coloured with the "Critical" or "Unstable" swatch — these represent the strike orientations most likely to be actively slipping or on the verge of Coulomb failure under the assumed stress state.

12.2 Example 2 — Comparing Frequency- vs. Length-Weighted Orientation Distributions

  1. Draw the rose with Weight by = Count to see which strike bins contain the most individual fracture segments.
  2. Redraw with Weight by = Length; note any change in the dominant petal — a strike bin with few but very long fractures may become dominant under length weighting even though it was subordinate under count weighting.
  3. Use this comparison to decide whether a proposed fracture set is genuinely volumetrically significant or is merely numerically prevalent due to digitisation of many short segments.
Count-weighted equal-area rose from the SVG dataset with normalized slip-tendency colours
Figure 12.1a — Count-weighted SVG rose: each of the 2,023 calculated segments contributes equally to its strike bin. Petal area represents relative segment frequency, while colour represents the bin's normalised slip tendency. Compare with Figure 12.1b using the same 36-bin setting and stress state (100/50/25 MPa; cohesion and pore pressure 0 MPa; friction 0.6). Opposite petals represent the same axial orientation.
Length-weighted equal-area rose of the same SVG network for comparison with Count weighting
Figure 12.1b — Length-weighted SVG rose with the data, bin count, and stresses unchanged from Figure 12.1a. Each segment now contributes its physical length, so changes in petal area reveal the influence of longer traces rather than a change in stress. For this dataset the dominant axial orientation remains near 120°/300°; the differences in secondary petals are modest.

12.3 Example 3 — Publication-Quality Export

  1. Set the desired custom title via the Custom title field (e.g., "Well A-1 — Fracture Rose Coloured by Normalized Slip Tendency").
  2. Adjust Outer/Inner background colours, boundary width, and petal edge colour to match the target journal's figure style.
  3. Set Colour Bar → Position to Right and Length to approximately 0.6 for a compact vertical legend suitable for a two-column layout.
  4. Click Save Image; the resulting PNG is written at the application-configured export DPI to the project's FracSTAT_plot_exports directory, ready for direct inclusion in a manuscript or report.

13. Geological Interpretation

The Rose Tab's combined orientation/mechanics encoding directly supports several core geomechanical workflows in fracture-network characterisation and reservoir/rock-slope stability analysis.

13.1 Reactivation-Prone Set Identification

Under Andersonian faulting theory (Anderson, 1951), fractures oriented at the Coulomb-optimal angle to σ1 (approximately 2θ_opt = arctan(1/μ) from σ1, i.e. roughly 25°–35° for typical friction coefficients) exhibit the highest ratio of shear to effective normal stress and are therefore the most prone to frictional reactivation (Jaeger et al., 2007). A petal that is both prominent (high frequency or cumulative length) and coloured toward high slip tendency or the "Critical"/"Unstable" classification at approximately this optimal angular offset is a strong candidate for active or potentially active slip under the assumed stress state (Morris et al., 1996).

13.2 Dilation and Fluid-Flow Pathways

When Map Display is set to Dilation tendency, petals coloured toward high Td highlight strike sets that are mechanically favoured to open (dilate) rather than shear under the current stress field — these orientations are of particular interest for identifying preferential fluid-flow pathways or high-permeability fracture corridors in naturally fractured reservoirs, since dilated fractures typically exhibit enhanced hydraulic aperture relative to shear-dominated sets (Ferrill and Morris, 2003; Zoback, 2007).

13.3 Fracture Susceptibility as a Composite Risk Indicator

Fracture susceptibility Sf combines the normal-stress clamping effect, pore pressure, cohesion, and friction into a single margin-to-failure metric (see Section 5 of the Method tab documentation for the full formula), read most naturally as the pore-pressure increase a plane can tolerate before it slips (Streit and Hillis, 2004). Colouring the rose by susceptibility allows an analyst to rank strike sets by their overall geomechanical risk in a single view, complementing the four-tier Critical fractures classification with a continuous gradation useful for sensitivity or what-if stress-scenario comparisons.

13.4 Comparing Geometric vs. Mechanical Fabric

Because the Static Analyzer's own Rose Plot tab depicts the same fracture population's purely geometric strike distribution (uncoloured by any stress attribute), a useful diagnostic workflow is to compare the Static Analyzer's rose against the Dynamic Analyzer's stress-coloured rose for the same dataset: strike sets that are geometrically minor but mechanically critical may represent an under-sampled but geomechanically important fracture population warranting additional field verification or targeted sampling.

14. Implementation Notes

14.1 Class and File Organisation

RoseTab is implemented as a Python mixin class (no independent __init__ or base class of its own) intended solely to be combined via multiple inheritance into SlipTendencyApp alongside MapTab, MohrTab, TableTab, MethodTab, and StereonetTab. This mixin pattern is used consistently throughout FracSTAT's Dynamic and Static Analyzer workspaces to keep each tab's construction and drawing logic in its own source file while sharing a single top-level application-state object.

14.2 Variable Naming Convention

Every ui_common.qtk variable owned by this tab is prefixed rose_ (e.g., rose_bins_var, rose_show_circles_var) to avoid collisions with the many other tabs' variables that coexist on the same SlipTendencyApp instance.

14.3 Defensive Parsing

All free-text numeric entries (bin count, petal gap, circle line width, σ1 line width, colour-bar length, boundary width) are parsed with a try/except ValueError pattern that falls back to a hard-coded sane default rather than raising an exception into the Qt event loop, ensuring the application never crashes from a malformed or empty text entry.

14.4 Unused Helper Function

The module defines a small utility, _hex_to_rgb01(hex_color), which converts a '#RRGGBB' string into a normalised (r, g, b) tuple in the [0, 1] range. This helper is not currently invoked anywhere within the drawing pipeline (all colour handling is delegated to matplotlib's native hex-colour support) and is retained in the source as a utility available for any future feature requiring raw RGB tuples.

14.5 Dependency Summary

LibraryRole
numpyWeighted histogram binning, angular array manipulation, trigonometric conversions.
matplotlib (pyplot Figure/Axes API)Polar plot rendering, colour mapping (ScalarMappable), colour-bar construction.
matplotlib.backends.backend_qtaggEmbeds the Matplotlib Figure inside the Qt canvas widget (FigureCanvasTkAgg, the ui_common.qtk shim over FigureCanvasQTAgg).
ui_common.qtk (ctk)All settings-panel widgets: frames, labels, entries, checkboxes, option menus, buttons.
ui_common.ui_widgetsSupplies _StaticColorButton, the shared swatch/hex colour-picker widget.

15. Limitations

LimitationDescription
Unweighted colour aggregationPer-bin colour aggregation uses a simple arithmetic mean with no weighting by segment length or count, which can make small-population bins visually as prominent in colour intensity as large, statistically robust bins; users must cross-reference petal size when interpreting colour.
Compass labels tied to Half rose toggleThe compass N/E/S/W annotations are suppressed for half-rose diagrams, which is an intentional design choice (the half rose only spans 0–180° and W/S annotations would be geometrically meaningless in that mode), but is not configurable independently of the Half rose toggle.
No orientation-bias correctionEvery fracture contributes with equal weight regardless of the angle at which the sampling line or outcrop face intersects it, so sets sub-parallel to the sampling direction are systematically under-represented. No Terzaghi weighting is applied (Terzaghi, 1965).
No circular summary statistics or significance testsThe tab is purely graphical: it reports no mean strike, resultant length, circular variance, or test of uniformity, all of which would be needed to state whether an apparent preferred orientation is statistically significant (Mardia and Jupp, 2000; Fisher, 1993).
Bin count is a free parameterPetal count is set by hand with no data-driven rule. Because a rose diagram's apparent modality can change materially with bin width, the chosen bin count should always be reported and ideally varied to confirm that a peak is robust (Fisher, 1993).
Strike only, dip discardedFolding to a 0–180° strike domain (Section 5.1) collapses the full three-dimensional plane orientation onto a single azimuth, so two sets sharing a strike but dipping oppositely are indistinguishable here. Use the Stereonet tab where dip matters (Davis et al., 2011).
Colour scale is dataset-relative for susceptibilityFractureSusceptibilityNorm is rescaled and inverted against the loaded file's own minimum and maximum (Section 5.5), so a saturated petal marks the least stable orientation in that file, not one at failure. Values are not comparable between datasets.

16. References

Sources are listed alphabetically within each group. In-text citations throughout this page link to the corresponding entry below.

16.1 Circular and Directional Statistics

16.2 Structural Geology, Stress and Reactivation

  • Anderson, E. M. (1951). The Dynamics of Faulting and Dyke Formation with Applications to Britain (2nd ed.). Oliver and Boyd, Edinburgh.
  • Davis, G. H., Reynolds, S. J., & Kluth, C. F. (2011). Structural Geology of Rocks and Regions (3rd ed.). John Wiley & Sons. ISBN 978-0-471-15231-6.
  • Ferrill, D. A., & Morris, A. P. (2003). Dilational normal faults. Journal of Structural Geology, 25(2), 183–196. https://doi.org/10.1016/S0191-8141(02)00029-9
  • Jaeger, J. C., Cook, N. G. W., & Zimmerman, R. W. (2007). Fundamentals of Rock Mechanics (4th ed.). Blackwell Publishing, Oxford. ISBN 978-0-632-05759-7.
  • Morris, A., Ferrill, D. A., & Henderson, D. B. (1996). Slip-tendency analysis and fault reactivation. Geology, 24(3), 275–278. https://doi.org/10.1130/0091-7613(1996)024<0275:STAAFR>2.3.CO;2
  • Streit, J. E., & Hillis, R. R. (2004). Estimating fault stability and sustainable fluid pressures for underground storage of CO₂ in porous rock. Energy, 29(9–10), 1445–1456. https://doi.org/10.1016/j.energy.2004.03.078
  • Zoback, M. D. (2007). Reservoir Geomechanics. Cambridge University Press. https://doi.org/10.1017/CBO9780511586477

16.3 Software

Methods statement for the Rose Tab. If a figure produced by this tab appears in a publication, the details below are what a reader needs in order to reproduce it, and are what the citations above assume:

Equal-area (square-root radius) rose diagram of fracture strike, folded to the 0–180° axial domain (Mardia and Jupp, 2000), computed with B equal-width bins and weighted by trace length (or by count, as stated) from n observations, drawn as a half or mirrored full rose as stated. Petals coloured by the bin-mean value of the selected stress-tendency metric — slip tendency (Morris et al., 1996), dilation tendency (Ferrill and Morris, 2003), fracture susceptibility (Streit and Hillis, 2004), or the four-tier critical-state code — for σ₁/σ₂/σ₃ = … MPa, P_f = … MPa, C₀ = … MPa and μ = … (Jaeger et al., 2007). Implemented in NumPy (Harris et al., 2020) and rendered with Matplotlib (Hunter, 2007). Report B, n, the weighting mode, the stress tensor, and the colour metric; note that petal colour is an unweighted bin mean (Section 8.3) and that no orientation-bias correction is applied (Section 15).