Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify coverage plots #1

Open
donkirkby opened this issue Jul 26, 2019 · 1 comment
Open

Simplify coverage plots #1

donkirkby opened this issue Jul 26, 2019 · 1 comment

Comments

@donkirkby
Copy link
Contributor

I created a subclass of Coverage in the MiCall project. It simplifies the coverage plots by joining all the rectangles that are within 10% of the starting coverage value. That reduces most SVG file sizes to less than 10% of their current size.

Let me know if you like the technique, and I'll create a pull request for you to put this feature in Coverage. The next class in that file is ShadedCoverage to set colour based on coverage depth. You're welcome to that as well, but it might not be as generally useful. It also introduces a matplotlib dependency, although I could probably work around that if needed.

I hope your adventure is not too adventurous. Talk to you later.

jeff-k pushed a commit that referenced this issue Aug 1, 2023
@jeff-k
Copy link
Owner

jeff-k commented Aug 16, 2023

Hey @donkirkby, thanks a lot for this (sorry for the delayed response). I've been doing some aggressive refactoring to get this repo consistent with mypy, pylint, pytest, pyproject.toml, and black conventions while preserving backwards compatibility. Once the outstanding issues have been resolved I plan to redesign the class hierarchy. For example Tracks and Multitracks should not be separate classes, rather a Track should have its own list of child Regions.

For now I've committed the functionality you describe with a couple tweaks:

  • When the number of datapoints in the coverage graph is greater than the displayed size of the coverage track, the datapoints are "compressed" by averaging them:
    def _downsample(self, display_width: float) -> list[float]:
  • Since the y-axis is quite compressed, adjacent bars that would be displayed at visually indistinct pixel heights are merged into a wider rectangle (very similar to your approach):
    def _merge_bars(ys: list[float], yscale: float) -> list[tuple[float, float]]:

I've played around a bit in a jupyter notebook to test this and I think it's working. Here's an example:

ys = []
for x in range(0, 9719):
    y = math.sin(x/130) + 1.5
    y *= math.sin((x + 50)/ 160) + 1
    ys.append(y)
        
f.add(Coverage(0, 9716, ys))
d = f.show(w=10000)

Gives a file with these bars:

<rect x="0.0" y="-4.192857562809168" width="15" height="4.192857562809168" fill="blue" fill-opacity="1.0" />
<rect x="15.0" y="-4.993985684696245" width="25" height="4.993985684696245" fill="blue" fill-opacity="1.0" />
<rect x="40.0" y="-6.007016099352329" width="24" height="6.007016099352329" fill="blue" fill-opacity="1.0" />
<rect x="64.0" y="-7.009043771429525" width="25" height="7.009043771429525" fill="blue" fill-opacity="1.0" />
<rect x="89.0" y="-8.014022777463433" width="28" height="8.014022777463433" fill="blue" fill-opacity="1.0" />

And when rendered with

d = f.show(w=1000)

Outputs:

<rect x="0.0" y="-4.251865729537586" width="2" height="4.251865729537586" fill="blue" fill-opacity="1.0" />
<rect x="2.0" y="-4.972685079802743" width="2" height="4.972685079802743" fill="blue" fill-opacity="1.0" />
<rect x="4.0" y="-5.903159773258937" width="3" height="5.903159773258937" fill="blue" fill-opacity="1.0" />
<rect x="7.0" y="-7.008558308764625" width="3" height="7.008558308764625" fill="blue" fill-opacity="1.0" />
<rect x="10.0" y="-8.03227115614636" width="3" height="8.03227115614636" fill="blue" fill-opacity="1.0" />

Sorry that this commit doesn't give you credit for this feature. If you would like to commit the ShadedCoverage subclass I'll put it in now. I'm thinking in the future when I redesign the API to generalise the shading subclass into a more general decorator pattern. Something like this suggestion from chatgpt-4:

class ElementDecorator(Element):
    def __init__(self, element: Element):
        self._element = element

    def _draw_elements(self, group: draw.Group, xscale: float) -> draw.Group:
        return self._element._draw_elements(group, xscale)

    def draw(self, x: float = 0, y: float = 0, xscale: float = 1.0) -> draw.Group:
        return self._element.draw(x, y, xscale)


class ShadedColorDecorator(ElementDecorator):
    def __init__(self, element: Element):
        super().__init__(element)
        self.cm = cm.viridis_r
        self.normalize = Normalize(0, 6)

    def get_color(self, coverage: float) -> str:
        log_coverage = log10(coverage)
        rgba = self.cm(self.normalize(log_coverage))
        return colors.to_hex(rgba)

    def _draw_elements(self, group: draw.Group, xscale: float) -> draw.Group:
        # Modify the behavior of the _draw_elements method to use the new get_color method
        # This can be done by calling the original _draw_elements method and then modifying the color of the elements
        # Or by copying the logic from the original method and replacing the color assignment part
        # For simplicity, we'll assume the former approach here
        group = super()._draw_elements(group, xscale)
        for rect in group.children:
            coverage_value = rect.height / xscale  # Assuming this gives the original coverage value
            rect.fill = self.get_color(coverage_value)
        return group

Though I don't know when I'll get around to this, so the ShadedCoverage subclass would be good to have now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants