localize charts use fractional values on a percent axis and fail for sparse data
Summary
modkit localize --chart labels its y-axis as percent modified but plots fractional values, so a TSV value of 50 is rendered at 0.5. Chart construction also rejects a one-datum input, gives an invalid zero-width x-axis when multiple retained data share one offset, hides one-point series, and can create or truncate the chart destination before rendering later fails.
Severity
Severity: Medium — scientific reporting and output reliability
Rationale: The chart presents modification levels 100-fold below the numeric percent scale stated by its label and TSV, which can mislead visual interpretation. The underlying aggregation and TSV values remain correct. Singleton/empty-data behavior is a bounded functional and file-preservation defect rather than loss of primary scientific results.
User and scientific impact
- Affected result or workflow: HTML charts produced by
localize, especially feature sets with one retained offset or one datum per series.
- Direction of error: 100-fold plotted-scale mismatch, failed or invisible sparse chart, and premature creation/truncation of a chart file.
- Likely exposure: the scale mismatch affects every nonzero chart; sparse rendering depends on retained data geometry.
- Detectability or workaround: users can compare the chart with the correct TSV percentages; adding unrelated offsets can avoid the singleton-axis error but changes the visualization.
Affected versions and environment
- Affected release: modkit 0.6.4.
- Development revision:
5cecc3fb3a9336068d9e3c68d5c08d678153dd2c.
- Operating system and architecture used for verification: macOS on Apple Silicon.
- Input format: bgzip-compressed, tabix-indexed bedMethyl plus BED regions and genome sizes.
Steps to reproduce
Create a minimal two-position bedMethyl input:
printf 'chr1\t9\t10\tm\t10\t+\t9\t10\t255,0,0\t10\t50.00\t5\t5\t0\t0\t0\t0\t0\nchr1\t10\t11\tm\t10\t+\t10\t11\t255,0,0\t10\t20.00\t2\t8\t0\t0\t0\t0\t0\n' > counts.bed
bgzip -c counts.bed > counts.bed.gz
tabix -f -p bed counts.bed.gz
printf 'chr1\t100\n' > genome.sizes
printf 'chr1\t10\t11\n' > region.bed
modkit localize counts.bed.gz \
--regions region.bed \
--genome-sizes genome.sizes \
--window 2 \
--min-coverage 0 \
--out-file out.tsv \
--chart chart.html \
--force
Observed behavior
The TSV reports percent-modified values of 50 and 20, while the HTML chart series contains fractional values 0.5 and 0.2 on an axis labeled as percent modified.
For the one-datum case, repeat the setup with only the first bedMethyl row. The affected command errors when minmax() returns OneElement, and chart creation may leave a zero-byte destination. With two or more retained entries/series sharing the sole offset, raw minimum and maximum bounds are equal, producing a zero-width axis. A one-datum series is also configured without a visible point symbol.
For the empty-data case, use this region:
printf 'chr1\t50\t51\n' > region.bed
If chart.html already exists, rendering failure can truncate it because the destination is opened before the chart has been constructed successfully.
Control or independent oracle
- The TSV's
percent_modified values are the chart's stated y-axis unit: 50 and 20 must be plotted as 50 and 20.
- A singleton x value requires distinct padded integer bounds and a visible marker.
- An empty data set should return a clear error without creating or modifying the requested chart file.
Expected behavior
- Chart series use
percent_modified() values, matching the TSV's numeric percent quantity and the y-axis label, allowing normal serialization-precision differences.
- A shared singleton offset uses saturating one-unit padding so the x-axis is valid.
- Every singleton series has a visible marker; multi-point line behavior remains unchanged.
- Empty chart data returns an error before chart creation.
- Chart rendering completes successfully in memory before the destination is created or truncated.
- TSV bytes and scientific aggregation remain unchanged.
Root-cause evidence
modkit-core/src/localise/util.rs: chart series use frac_modified() despite a percent label; x bounds are the raw min/max; series symbols are disabled; empty values are not rejected before bound construction.
modkit-core/src/localise/subcommand.rs: the chart destination is opened before get_plot completes.
- Focused parent-red/fix-green checks reproduce the 0.5-versus-50 mismatch, singleton failure/invisibility, and empty-data side effect while confirming identical TSV output.
Proposed fix scope
- Plot percent values.
- Validate nonempty chart input, pad singleton integer bounds, and show a point marker for one-datum series.
- Render the HTML string before opening the chart destination.
- Keep this chart-only correction separate from localize region parsing, regional-query propagation, and window/offset geometry fixes.
Non-goals
- No aggregation, coordinate, strand, threshold, schema, or TSV-byte change.
- No transactional rollback coupling the TSV and chart outputs.
- No complete-write, explicit flush/fsync, atomic-replacement, or compressed-output guarantee.
- Empty data still returns an error rather than producing a blank chart.
- No performance redesign.
Acceptance criteria
- Values plotted on the percent axis represent the same numeric percent quantity as the TSV, allowing normal serialization-precision differences.
- Empty, one-offset, mixed-singleton, and ordinary multi-offset fixtures have deterministic outcomes.
- Singleton bounds are valid and singleton series have visible markers.
- Empty data returns a clear error without creating or changing the chart target.
- Existing multi-offset bounds, multi-point behavior, TSV bytes, and aggregation remain unchanged.
- Focused chart/localize tests and the applicable full workspace test suite pass.
Reproduction artifacts
All fixtures are generated inline by the commands above.
Related work
localizecharts use fractional values on a percent axis and fail for sparse dataSummary
modkit localize --chartlabels its y-axis as percent modified but plots fractional values, so a TSV value of 50 is rendered at 0.5. Chart construction also rejects a one-datum input, gives an invalid zero-width x-axis when multiple retained data share one offset, hides one-point series, and can create or truncate the chart destination before rendering later fails.Severity
Severity: Medium — scientific reporting and output reliability
Rationale: The chart presents modification levels 100-fold below the numeric percent scale stated by its label and TSV, which can mislead visual interpretation. The underlying aggregation and TSV values remain correct. Singleton/empty-data behavior is a bounded functional and file-preservation defect rather than loss of primary scientific results.
User and scientific impact
localize, especially feature sets with one retained offset or one datum per series.Affected versions and environment
5cecc3fb3a9336068d9e3c68d5c08d678153dd2c.Steps to reproduce
Create a minimal two-position bedMethyl input:
Observed behavior
The TSV reports percent-modified values of 50 and 20, while the HTML chart series contains fractional values 0.5 and 0.2 on an axis labeled as percent modified.
For the one-datum case, repeat the setup with only the first bedMethyl row. The affected command errors when
minmax()returnsOneElement, and chart creation may leave a zero-byte destination. With two or more retained entries/series sharing the sole offset, raw minimum and maximum bounds are equal, producing a zero-width axis. A one-datum series is also configured without a visible point symbol.For the empty-data case, use this region:
If
chart.htmlalready exists, rendering failure can truncate it because the destination is opened before the chart has been constructed successfully.Control or independent oracle
percent_modifiedvalues are the chart's stated y-axis unit: 50 and 20 must be plotted as 50 and 20.Expected behavior
percent_modified()values, matching the TSV's numeric percent quantity and the y-axis label, allowing normal serialization-precision differences.Root-cause evidence
modkit-core/src/localise/util.rs: chart series usefrac_modified()despite a percent label; x bounds are the raw min/max; series symbols are disabled; empty values are not rejected before bound construction.modkit-core/src/localise/subcommand.rs: the chart destination is opened beforeget_plotcompletes.Proposed fix scope
Non-goals
Acceptance criteria
Reproduction artifacts
All fixtures are generated inline by the commands above.
Related work