diff --git a/.gitignore b/.gitignore index 79b5594..9810f5a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,9 @@ +js/* + **/.DS_Store + +!.gitignore +!python +python/sample_composites/composite_average.out +/python/sample_composites +/python/__pycache__ \ No newline at end of file diff --git a/js/parse_composite.js b/js/parse_composite.js index 11fb78b..3dde9e8 100644 --- a/js/parse_composite.js +++ b/js/parse_composite.js @@ -160,6 +160,5 @@ let parse_multiple_composite = function(text, prefix) { if (save_comp) { composites[id] = {xmin: xmin, xmax: xmax, sense: sense, anti: anti} }; - return composites } \ No newline at end of file diff --git a/js/widgets/main_plot.js b/js/widgets/main_plot.js index bad2615..d6f9ce1 100644 --- a/js/widgets/main_plot.js +++ b/js/widgets/main_plot.js @@ -171,8 +171,8 @@ $(function() { d3.select("#main-plot-div").on("mousemove", function(e) { $("#main-plot").main_plot("move_tooltip", e) }); - main_plot.on("mouseleave", function() { - // $("#main-plot").main_plot("hide_tooltip") + d3.select("#main-plot-div").on("mouseleave", function() { + $("#main-plot").main_plot("hide_tooltip") }); this.enable_tooltip = true; @@ -737,8 +737,10 @@ $(function() { if (color_trace) { this._elements.composite_group.selectAll(".composite .color-line-top") .style("display", null); - this._elements.composite_group.selectAll(".composite .color-line-bottom") - .style("display", null); + if (!this.combined){ + this._elements.composite_group.selectAll(".composite .color-line-bottom") + .style("display", null); + } this._elements.composite_group.selectAll(".composite .white-line") .style("display", "none"); this._elements.composite_group.selectAll(".composite .black-line") @@ -820,12 +822,13 @@ $(function() { .style("left", ev.clientX - (w - 80) / 1.4) } else { this._elements.tooltip.style("display", "none") + d3.selectAll("#composite-plot-tooltip").remove(); } } }, hide_tooltip: function() { - this._elements.tooltip.style("display", "none") + d3.selectAll("#composite-plot-tooltip").remove(); }, download_as_svg: function() { diff --git a/python/composite.py b/python/composite.py new file mode 100644 index 0000000..9112524 --- /dev/null +++ b/python/composite.py @@ -0,0 +1,87 @@ +import csv +import xml.dom.minidom as dom +import argparse +import math +import parseComposite + +# Objected to store bare-bones composite data, can be used with plot_composite function +class SimpleComposite: + def __init__(self, xmin=None, xmax=None, sense=[], anti=[], id=""): + self.xmin = xmin + self.xmax = xmax + self.sense = sense + self.anti = anti + self.id = id + +# Object to store composite data with options for plotting, similar to a settings row +class Composite: + def __init__(self, scale=1, color=None, secondary_color=None, i=None, opacity=None, smoothing=None, bp_shift=None, hide_sense=False, hide_anti=False, baseline=0, name=None, sense=None, anti=None, xmin=None, xmax=None): + # Sets default values + self.scale = scale if scale is not None else 1 + self.color = color if color is not None else "#0000FF" + self.secondary_color = secondary_color if secondary_color is not None else color + self.baseline = baseline if baseline is not None else 0 + self.xmin = xmin if xmin is not None else 0 + self.xmax = xmax if xmax is not None else 0 + self.sense = sense if sense is not None else [] + self.anti = anti if anti is not None else [] + # Don't assign defaults to opacity, smoothing, and bp_shift so plot can apply plot defaults + self.opacity = opacity + self.smoothing = smoothing + self.bp_shift = bp_shift + self.hide_anti = hide_anti + self.hide_sense = hide_sense + self.individual_files = {} + self.files_loaded = len(self.individual_files) + self.name = name + # Adds a simple composite to the 'row' + def load_simple_composite(self,composite: SimpleComposite): + # If no files, initialize sense and anti arrays; otherwise, pad sense and anti arrays to new xdomain + self.xmin = min(composite.xmin, self.xmin) + self.xmax = max(composite.xmax, self.xmax) + if len(self.individual_files) == 0: + self.sense = [0] * (composite.xmax - composite.xmin + 1) + self.anti = [0] * (composite.xmax - composite.xmin + 1) + else: + xmin = min([int(self.individual_files[c].xmin) for c in self.individual_files]) + xmax = max([int(self.individual_files[c].xmax) for c in self.individual_files]) + prefix = [0] * (xmin - self.xmin) + suffix = [0] * (self.xmax - xmax) + self.sense = prefix + self.sense + suffix + self.anti = prefix + self.anti + suffix + # Update sense and anti arrays + j = composite.xmin - self.xmin + while j <= composite.xmax - composite.xmin: + idx = composite.xmin - self.xmin + j + self.sense[idx] += composite.sense[j] + self.anti[idx] += composite.anti[j] + j += 1 + self.individual_files[composite.id] = composite + # Loads dictionary from parse_multiple_composites + def load_composite_dict(self,composite_dict: dict): + for key in composite_dict: + composite = composite_dict[key] + # If no files, initialize sense and anti arrays; otherwise, pad sense and anti arrays to new xdomain + self.xmin = min(composite.xmin, self.xmin) + self.xmax = max(composite.xmax, self.xmax) + if len(self.individual_files) == 0: + self.sense = [0] * (composite.xmax - composite.xmin + 1) + self.anti = [0] * (composite.xmax - composite.xmin + 1) + else: + xmin = min([c.xmin for c in self.individual_files]) + xmax = max([c.xmax for c in self.individual_files]) + prefix = [0] * (xmin - self.xmin) + suffix = [0] * (self.xmax - xmax) + self.sense = prefix + self.sense + suffix + self.anti = prefix + self.anti + suffix + # Update sense and anti arrays + j = composite.xmin - self.xmin + while j <= composite.xmax - composite.xmin: + idx = composite.xmin - self.xmin + j + self.sense[idx] += composite.sense[j] + self.anti[idx] += composite.anti[j] + j += 1 + self.individual_files[composite.id] = composite + + def __str__(self): + return str(self.individual_files) \ No newline at end of file diff --git a/python/out.svg b/python/out.svg new file mode 100644 index 0000000..3831ed0 --- /dev/null +++ b/python/out.svg @@ -0,0 +1,123 @@ + + -4.6 + + Only the biggest PLOT + Position (bp) + + okay X10 + -1 + + -549 + 550 + 4.7 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + 1_imported + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/parseComposite.py b/python/parseComposite.py new file mode 100644 index 0000000..ccbaaf3 --- /dev/null +++ b/python/parseComposite.py @@ -0,0 +1,144 @@ +import csv +import xml.dom.minidom as dom +import argparse +import math +import composite +import os + +# Returns a simple composite from a single file +def parse_simple(file): + fileArr = open(file, "r").read().split("\n") + xmin = None + xmax = None + sense = [] + anti = [] + xmin_curr = 0 + xmax_curr = 0 + offset = 0 + for line in fileArr: + # Skip empty + if len(line.strip()) == 0 : + continue + # Separate fields + fields = line.split("\t") + if not fields[0].strip() or fields[0] == "NAME": + xmin_curr = int(float(fields[1])) + xmax_curr = int(float(fields[-1])) + # If the x domain starts at 0 shift it to the left + if xmin_curr == 0: + xmin_curr -= math.floor(xmax_curr / 2) + xmax_curr -= math.floor(xmax_curr / 2) + # If the x domain is not defined yet, define it + if xmin == None or xmax == None: + xmin = xmin_curr + xmax = xmax_curr + # Redefine min and max if necessary + xmax = max(xmax_curr, xmax) + xmin = min(xmin_curr, xmin) + sense = [0] * (xmax - xmin + 1) + anti = [0] * (xmax - xmin + 1) + # Add the values to sense and anti arrays + if "sense" in fields[0].lower(): + i = 1 + while i < len(fields): + sense[offset + i - 1] += float(fields[i]) + i += 1 + elif "anti" in fields[0].lower(): + i = 1 + while i < len(fields): + anti[offset + i - 1] += float(fields[i]) + i += 1 + # If the first field is not empty or "NAME" and does not contain "sense" or "anti" parse as combined or midpoint data + elif not (fields[0] == "" or fields[0] == "NAME"): + i = 1 + while i < len(fields): + sense[offset + i - 1] += float(fields[i]) / 2 + anti[offset + i - 1] += float(fields[i]) / 2 + return composite.SimpleComposite(xmin, xmax, sense, anti, os.path.basename(file).split('_')[0]) + +# Returns list of prefixes from multi-composite file, mimics the plotter method +def get_prefixes_from_multiple_composites(file): + lines = open(file, "r").read().split("\n") + names_list = [] + i = 0 + while i < len(lines): + line = lines[i] + # Skip empty + if line.strip() == "": + i += 1 + continue + # Get the first field + col0 = line.split("\t")[0] + if col0 == "" or col0[0] == "NAME": + # Get the names of the composites for lines immediately following the xdomain + i += 1 + names_list.append(lines[i].split("\t")[0]) + i += 1 + # Take the first name and split it by "_" + split_name = names_list[0].split("_") + idx = None + # Iterate over each possible prefix-suffix split + for i in range(1, len(split_name) - 1): + prefix = "_".join(split_name[:i]) + suffix = "_".join(split_name[i:]) + n_prefix = sum(1 for n in names_list if n.startswith(prefix)) + n_suffix = sum(1 for n in names_list if n.endswith(suffix)) + if n_prefix * n_suffix == len(names_list): + if n_suffix == len(names_list): + idx = i if idx is None else idx + break + idx = i + suffix = "_".join(split_name[idx:]) + # Get the prefixes by removing the suffix from the names + return [n[:-len(suffix)] for n in names_list if n.endswith(suffix)] + +# Returns dictionary with composite from multi-composite file, mimics the plotter method +def parse_multiple_composite(file, prefix): + lines = open(file, "r").read().split("\n") + composites = {} + xmin = None + xmax = None + sense = [] + anti = [] + i = 0 + id = 0 + save_comp = False + while i < len(lines): + line = lines[i] + # Skip empty + if line.strip() == "": + i += 1 + continue + # Get the first field + fields = line.split("\t") + col0 = fields[0] + if not col0.strip() or col0 == "NAME": + # If the x domain is defined, save the composite + if save_comp: + composites[id] = composite.SimpleComposite(xmin, xmax, sense, anti, id) + save_comp = False + # Get the nex x domain + fields = [field for field in fields if field.strip()] + xmin = int(float(fields[0])) + xmax = int(float(fields[-1])) + # If the x domain starts at 0 shift it to the left + if xmin == 0: + xmin -= math.floor(xmax / 2) + xmin -= math.floor(xmax / 2) + elif col0.startswith(prefix): + id = col0[len(prefix):].split("_")[0] + save_comp = True + # Add the values to sense and anti arrays + fields = [field for field in fields if field.strip()] + if "sense" in fields[0].lower(): + sense = [float(val) for val in fields[1:]] + elif "anti" in fields[0].lower(): + anti = [float(val) for val in fields[1:]] + else: + sense = [float(val) / 2 for val in fields[1:]] + anti = [float(val) / 2 for val in fields[1:]] + i += 1 + # Save the last composite + if save_comp: + composites[id] = composite.SimpleComposite(xmin, xmax, sense, anti, id) + return composites \ No newline at end of file diff --git a/python/plot.py b/python/plot.py new file mode 100644 index 0000000..7360905 --- /dev/null +++ b/python/plot.py @@ -0,0 +1,506 @@ +import xml.dom.minidom as dom +import copy +import slidingWindow +import math +import json +import composite + +document = dom.Document() + +# Class that generates composite and reference lines svg elements +class Plot: + def __init__(self, title=None, xmin=None, xmax=None, ymin=None, ymax=None, xlabel=None, ylabel=None, + opacity=None, smoothing=None, bp_shift=None, combined=False, color_trace=False, hide_legend=False): + # Set variables to defaults if argument passed into constructor was None + self.title = title if title is not None else "Composite plot" + self.xmin = xmin if xmin is not None else -500 + self.xmax = xmax if xmax is not None else 500 + self.ymin = ymin if ymin is not None else -1 + self.ymax = ymax if ymax is not None else 1 + self.xlabel = xlabel if xlabel is not None else "Position (bp)" + self.ylabel = ylabel if ylabel is not None else "Occupancy (AU)" + self.opacity = opacity if opacity is not None else 1 + self.smoothing = smoothing if smoothing is not None else 7 + self.bp_shift = bp_shift if bp_shift is not None else 0 + self.combined = combined + self.color_trace = color_trace + self.hide_legend = hide_legend + # Set dimensions to same constants as plotter + self.width = 460 + self.height = 300 + self.margins = {'top': 30, 'right': 170, 'bottom': 35, 'left': 40} + # Create groups for adding composites and reference lines + self.plot = document.createElement("g") + self.composite_group = document.createElement("g") + self.composite_group.setAttribute("class", "composite plotted") + self.gradients_group = document.createElement("defs") + self.reference_group = document.createElement("g") + self.xscale = XScale(self) + self.yscale = YScale(self) + self.num_composites = 0 + self.composites = [] + self.styles ={"dashed" : "5,5", + "solid" : "0", + "dotted" : "2,1"} + + # Creates a composite svg element from separate sense and anti arrays - mimics plot_composite from plotter + def plot_composite(self, xmin, xmax, sense, anti, scale=1, color=None, secondary_color=None, i=None, opacity=None, smoothing=None, bp_shift=None, hide_sense=False, hide_anti=False, baseline=0): + # Set parameters to global values if not specified + opacity = opacity if opacity is not None else self.opacity + smoothing = smoothing if smoothing is not None else self.smoothing + bp_shift = bp_shift if bp_shift is not None else self.bp_shift + if (i is None): + i = self.num_composites + self.num_composites += 1 + else: + i = i + # Set x domain as array of integers from xmin to xmax + xdomain = [i + xmin for i in range(xmax - xmin + 1)] + if (self.combined): + # Calculate defined x domain after shifting + shifted_xdomain = [x for x in xdomain if x - bp_shift >= xdomain[0] and x - bp_shift <= xdomain[-1] + and x + bp_shift >= xdomain[0] and x + bp_shift <= xdomain[-1]] + shifted_sense = [sense[j] for j in range(len(sense)) if xdomain[j] + bp_shift >= shifted_xdomain[0] + and xdomain[j] + bp_shift <= shifted_xdomain[-1]] + shifted_anti = [anti[j] for j in range(len(anti)) if xdomain[j] + bp_shift >= shifted_xdomain[0] + and xdomain[j] + bp_shift <= shifted_xdomain[-1]] + # Add occupancy for sense and anti + combined_occupancy = [shifted_sense[j] + shifted_anti[j] for j in range(len(shifted_sense))] + # Smooth occupancy with moving average + new_xdomain, smoothed_occupancy = slidingWindow.sliding_window(shifted_xdomain, combined_occupancy, smoothing).values() + # Truncate x domain to x axis limits + truncated_xdomain = [x for x in new_xdomain if x >= self.xmin and x <= self.xmax] + # Truncate occupancy and scale by scale factor, adding baseline value + scaled_occupancy = [value if (value := d * scale + baseline) > 0 else 0 for j, d in enumerate(smoothed_occupancy) + if int(new_xdomain[j]) >= self.xmin and int(new_xdomain[j]) <= self.xmax] + composite_fill_top = document.createElement("polygon") + composite_fill_top.setAttribute("points", " ".join(points := [f"{self.xscale.get(d)},{self.yscale.get(scaled_occupancy[j])}" for j, d in enumerate(truncated_xdomain)]) + f" {self.xscale.get(truncated_xdomain[-1])},{self.yscale.get(0)} {self.xscale.get(truncated_xdomain[0])},{self.yscale.get(0)}") + composite_fill_top.setAttribute("fill", "url(#composite-gradient-top" + str(i) + ")") + self.composite_group.appendChild(composite_fill_top) + #Create outline + wide_trace = document.createElement("path") + wide_trace.setAttribute("stroke-width", "1") + wide_trace.setAttribute("stroke", color) + wide_trace.setAttribute("fill", "none") + wide_trace.setAttribute("d", "M" + "L".join(points)) + self.composite_group.appendChild(wide_trace) + if not self.color_trace: + wide_trace.setAttribute("stroke", "#FFFFFF") + narrow_trace = copy.deepcopy(wide_trace) + narrow_trace.setAttribute("stroke-width", "0.5") + narrow_trace.setAttribute("stroke", "#000000") + narrow_trace.setAttribute("d", "M" + "L".join(points)) + self.composite_group.appendChild(narrow_trace) + else: + # Smooth sense and anti occupancy with moving average + new_xdomain, smoothed_sense = slidingWindow.sliding_window(xdomain, sense, smoothing).values() + smoothed_anti = list(slidingWindow.sliding_window(xdomain, anti, smoothing).values())[1] + # Truncate x domain to x axis limits + truncated_sense_domain = [j for x in new_xdomain if (j := x + bp_shift) >= self.xmin and j <= self.xmax] + truncated_anti_domain = [j for x in new_xdomain if (j := x - bp_shift) >= self.xmin and j <= self.xmax] + # Truncate sense and anti occupancy and scale by scale factor + scaled_sense = [value if (value := d * scale + baseline) > 0 else 0 for j, d in enumerate(smoothed_sense) + if int(new_xdomain[j] + bp_shift) >= self.xmin and int(new_xdomain[j] + bp_shift) <= self.xmax] + scaled_anti = [value if (value := d * scale + baseline) > 0 else 0 for j, d in enumerate(smoothed_anti) + if int(new_xdomain[j] - bp_shift) >= self.xmin and int(new_xdomain[j] - bp_shift) <= self.xmax] + # Create sense trace and polygon if not hidden + if not hide_anti: + # Create top polygon + composite_fill_top = document.createElement("polygon") + composite_fill_top.setAttribute("points", " ".join(sense_points := [f"{self.xscale.get(d)},{self.yscale.get(scaled_sense[j])}" for j, d in enumerate(truncated_sense_domain)]) + f" {self.xscale.get(truncated_sense_domain[-1])},{self.yscale.get(0)} {self.xscale.get(truncated_sense_domain[0])},{self.yscale.get(0)}") + composite_fill_top.setAttribute("fill", "url(#composite-gradient-top" + str(i) + ")") + self.composite_group.appendChild(composite_fill_top) + #Create trace + top_wide_trace = document.createElement("path") + top_wide_trace.setAttribute("stroke-width", "1") + top_wide_trace.setAttribute("stroke", color) + top_wide_trace.setAttribute("fill", "none") + top_wide_trace.setAttribute("d", "M" + "L".join(sense_points)) + self.composite_group.appendChild(top_wide_trace) + if not self.color_trace: + top_wide_trace.setAttribute("stroke", "#FFFFFF") + top_narrow_trace = copy.deepcopy(top_wide_trace) + top_narrow_trace.setAttribute("stroke-width", "0.5") + top_narrow_trace.setAttribute("stroke", "#000000") + top_narrow_trace.setAttribute("d", "M" + "L".join(sense_points)) + self.composite_group.appendChild(top_narrow_trace) + # Create anti trace and polygon if not hidden + if not hide_anti: + # Create polygon + composite_fill_bottom = document.createElement("polygon") + composite_fill_bottom.setAttribute("points", " ".join(anti_points := [f"{self.xscale.get(d)},{self.yscale.get(-scaled_anti[j])}" for j, d in enumerate(truncated_anti_domain)]) + f" {self.xscale.get(truncated_anti_domain[-1])},{self.yscale.get(0)} {self.xscale.get(truncated_anti_domain[0])},{self.yscale.get(0)}") + composite_fill_bottom.setAttribute("fill", "url(#composite-gradient-bottom" + str(i) + ")") + self.composite_group.appendChild(composite_fill_bottom) + #Create trace + bottom_wide_trace = document.createElement("path") + bottom_wide_trace.setAttribute("stroke-width", "1") + bottom_wide_trace.setAttribute("stroke", secondary_color) + bottom_wide_trace.setAttribute("fill", "none") + bottom_wide_trace.setAttribute("d", "M" + "L".join(anti_points)) + self.composite_group.appendChild(bottom_wide_trace) + if not self.color_trace: + bottom_wide_trace.setAttribute("stroke", "#FFFFFF") + bottom_narrow_trace = copy.deepcopy(bottom_wide_trace) + bottom_narrow_trace.setAttribute("stroke-width", "0.5") + bottom_narrow_trace.setAttribute("stroke", "#000000") + bottom_narrow_trace.setAttribute("d", "M" + "L".join(anti_points)) + self.composite_group.appendChild(bottom_narrow_trace) + self.generateGradients(opacity, i, color, secondary_color=secondary_color) + self.plot.appendChild(self.gradients_group) + self.plot.appendChild(self.composite_group) + + # Creates a composite svg element from a composite object, like plotting a row form the settings table + def plot_composite(self, composite): + # Set parameters to global values if not specified + opacity = composite.opacity if composite.opacity is not None else self.opacity + smoothing = composite.smoothing if composite.smoothing is not None else self.smoothing + bp_shift = composite.bp_shift if composite.bp_shift is not None else self.bp_shift + i = self.num_composites + self.num_composites += 1 + # Set x domain as array of integers from xmin to xmax + xdomain = [i + composite.xmin for i in range(composite.xmax - composite.xmin + 1)] + if (self.combined): + # Calculate defined x domain after shifting + shifted_xdomain = [x for x in xdomain if x - bp_shift >= xdomain[0] and x - bp_shift <= xdomain[-1] + and x + bp_shift >= xdomain[0] and x + bp_shift <= xdomain[-1]] + shifted_sense = [composite.sense[j] for j in range(len(composite.sense)) if xdomain[j] + bp_shift >= shifted_xdomain[0] + and xdomain[j] + bp_shift <= shifted_xdomain[-1]] + shifted_anti = [composite.anti[j] for j in range(len(composite.anti)) if xdomain[j] + bp_shift >= shifted_xdomain[0] + and xdomain[j] + bp_shift <= shifted_xdomain[-1]] + # Add occupancy for sense and anti + combined_occupancy = [shifted_sense[j] + shifted_anti[j] for j in range(len(shifted_sense))] + # Smooth occupancy with moving average + new_xdomain, smoothed_occupancy = slidingWindow.sliding_window(shifted_xdomain, combined_occupancy, smoothing).values() + # Truncate x domain to x axis limits + truncated_xdomain = [x for x in new_xdomain if x >= self.xmin and x <= self.xmax] + # Truncate occupancy and scale by scale factor, adding baseline value + scaled_occupancy = [value if (value := d * composite.scale + composite.baseline) > 0 else 0 for j, d in enumerate(smoothed_occupancy) + if int(new_xdomain[j]) >= self.xmin and int(new_xdomain[j]) <= self.xmax] + composite_fill_top = document.createElement("polygon") + composite_fill_top.setAttribute("points", " ".join(points := [f"{self.xscale.get(d)},{self.yscale.get(scaled_occupancy[j])}" for j, d in enumerate(truncated_xdomain)]) + f" {self.xscale.get(truncated_xdomain[-1])},{self.yscale.get(0)} {self.xscale.get(truncated_xdomain[0])},{self.yscale.get(0)}") + composite_fill_top.setAttribute("fill", "url(#composite-gradient-top" + str(i) + ")") + self.composite_group.appendChild(composite_fill_top) + #Create outline + wide_trace = document.createElement("path") + wide_trace.setAttribute("stroke-width", "1") + wide_trace.setAttribute("stroke", composite.color) + wide_trace.setAttribute("fill", "none") + wide_trace.setAttribute("d", "M" + "L".join(points)) + self.composite_group.appendChild(wide_trace) + if not self.color_trace: + wide_trace.setAttribute("stroke", "#FFFFFF") + narrow_trace = copy.deepcopy(wide_trace) + narrow_trace.setAttribute("stroke-width", "0.5") + narrow_trace.setAttribute("stroke", "#000000") + narrow_trace.setAttribute("d", "M" + "L".join(points)) + self.composite_group.appendChild(narrow_trace) + else: + # Smooth sense and anti occupancy with moving average + new_xdomain, smoothed_sense = slidingWindow.sliding_window(xdomain, composite.sense, smoothing).values() + smoothed_anti = list(slidingWindow.sliding_window(xdomain, composite.anti, smoothing).values())[1] + # Truncate x domain to x axis limits + truncated_sense_domain = [j for x in new_xdomain if (j := x + bp_shift) >= self.xmin and j <= self.xmax] + truncated_anti_domain = [j for x in new_xdomain if (j := x - bp_shift) >= self.xmin and j <= self.xmax] + # Truncate sense and anti occupancy and scale by scale factor + scaled_sense = [value if (value := d * composite.scale + composite.baseline) > 0 else 0 for j, d in enumerate(smoothed_sense) + if int(new_xdomain[j] + bp_shift) >= self.xmin and int(new_xdomain[j] + bp_shift) <= self.xmax] + scaled_anti = [value if (value := d * composite.scale + composite.baseline) > 0 else 0 for j, d in enumerate(smoothed_anti) + if int(new_xdomain[j] - bp_shift) >= self.xmin and int(new_xdomain[j] - bp_shift) <= self.xmax] + # Create sense trace and polygon if not hidden + if not composite.hide_sense: + # Create top polygon + composite_fill_top = document.createElement("polygon") + composite_fill_top.setAttribute("points", " ".join(sense_points := [f"{self.xscale.get(d)},{self.yscale.get(scaled_sense[j])}" for j, d in enumerate(truncated_sense_domain)]) + f" {self.xscale.get(truncated_sense_domain[-1])},{self.yscale.get(0)} {self.xscale.get(truncated_sense_domain[0])},{self.yscale.get(0)}") + composite_fill_top.setAttribute("fill", "url(#composite-gradient-top" + str(i) + ")") + self.composite_group.appendChild(composite_fill_top) + #Create trace + top_wide_trace = document.createElement("path") + top_wide_trace.setAttribute("stroke-width", "1") + top_wide_trace.setAttribute("stroke", composite.color) + top_wide_trace.setAttribute("fill", "none") + top_wide_trace.setAttribute("d", "M" + "L".join(sense_points)) + self.composite_group.appendChild(top_wide_trace) + if not self.color_trace: + top_wide_trace.setAttribute("stroke", "#FFFFFF") + top_narrow_trace = copy.deepcopy(top_wide_trace) + top_narrow_trace.setAttribute("stroke-width", "0.5") + top_narrow_trace.setAttribute("stroke", "#000000") + top_narrow_trace.setAttribute("d", "M" + "L".join(sense_points)) + self.composite_group.appendChild(top_narrow_trace) + # Create anti trace and polygon if not hidden + if not composite.hide_anti: + # Create polygon + composite_fill_bottom = document.createElement("polygon") + composite_fill_bottom.setAttribute("points", " ".join(anti_points := [f"{self.xscale.get(d)},{self.yscale.get(-scaled_anti[j])}" for j, d in enumerate(truncated_anti_domain)]) + f" {self.xscale.get(truncated_anti_domain[-1])},{self.yscale.get(0)} {self.xscale.get(truncated_anti_domain[0])},{self.yscale.get(0)}") + composite_fill_bottom.setAttribute("fill", "url(#composite-gradient-bottom" + str(i) + ")") + self.composite_group.appendChild(composite_fill_bottom) + #Create trace + bottom_wide_trace = document.createElement("path") + bottom_wide_trace.setAttribute("stroke-width", "1") + bottom_wide_trace.setAttribute("stroke", composite.secondary_color) + bottom_wide_trace.setAttribute("fill", "none") + bottom_wide_trace.setAttribute("d", "M" + "L".join(anti_points)) + self.composite_group.appendChild(bottom_wide_trace) + if not self.color_trace: + bottom_wide_trace.setAttribute("stroke", "#FFFFFF") + bottom_narrow_trace = copy.deepcopy(bottom_wide_trace) + bottom_narrow_trace.setAttribute("stroke-width", "0.5") + bottom_narrow_trace.setAttribute("stroke", "#000000") + bottom_narrow_trace.setAttribute("d", "M" + "L".join(anti_points)) + self.composite_group.appendChild(bottom_narrow_trace) + self.generateGradients(opacity, i, composite.color, secondary_color=composite.secondary_color) + self.plot.appendChild(self.gradients_group) + self.plot.appendChild(self.composite_group) + + # Changes values and updates scale objects + def scale_axes(self, xmin=None, xmax=None, ymin=None, ymax=None): + self.xmin = xmin if xmin is not None else self.xmin + self.xmax = xmax if xmax is not None else self.xmax + self.ymin = ymin if ymin is not None else self.ymin + self.ymax = ymax if ymax is not None else self.ymax + self.xscale = XScale(self) + self.yscale = YScale(self) + + # Finds the max/min x and y values from composites on plot and scales axes accordingly + def autoscale_axes(self, allow_shrink): + xmin = min([group.xmin for group in self.composites]) + xmax = max([group.xmax for group in self.composites]) + if self.combined: + ymin = 0 + ymax = round(max([(group.sense[i] + group.sense[i]) * group.scale for group in self.composites for i in range(min(len(group.sense), len(group.anti)))]), 2) + else: + ymin = min([-val * group.scale for group in self.composites for val in group.anti]) + ymax = max([val * group.scale for group in self.composites for val in group.sense]) + self.scale_axes(xmin,xmax,ymin if allow_shrink else None,ymax if allow_shrink else None) + + # Adds composite group object to plot + def add_composite_group(self, composite_group): + if composite_group.name == None: + composite_group.name = self.num_composites + 1 + self.composites.append(composite_group) + + # Plots all composites on plot + def plot_composites(self): + for group in self.composites: + self.plot_composite(group) + return self.plot + + # Adds reference lines to plot + def plot_reference_line(self, axis=None, val=None, style=None, color=None, opacity=None): + # Sets default values for reference lines + axis = axis if axis is not None else "x" + val = val if val is not None else 0 + style = style if style is not None else "dashed" + color = color if color is not None else "#FF0000" + opacity = opacity if opacity is not None else 1 + bottom = self.height - self.margins.get('bottom') + top = self.margins.get("top") + right = self.width - (self.margins.get('right')) + left = self.margins.get("left") + # Draws reference lines on plot + line = document.createElement("line") + label = document.createElement("text") + if axis == "x": + val = int(val) + line = document.createElement("line") + line.setAttribute("x1", str(self.xscale.get(val))) + line.setAttribute("x2", str(self.xscale.get(val))) + line.setAttribute("y1", str(top)) + line.setAttribute("y2", str(bottom)) + label.setAttribute("x", str(self.xscale.get(val) - 4)) + label.setAttribute("y", str(bottom + 8)) + label.appendChild(document.createTextNode(str(val))) + elif axis == "y": + line = document.createElement("line") + line.setAttribute("x1", str(left)) + line.setAttribute("x2", str(right)) + line.setAttribute("y1", str(self.yscale.get(val))) + line.setAttribute("y2", str(self.yscale.get(val))) + label.setAttribute("x", str(right + 5)) + label.setAttribute("y", str(self.yscale.get(val) + 4)) + label.appendChild(document.createTextNode(str(val))) + line.setAttribute("stroke-dasharray", self.styles.get(style)) + line.setAttribute("stroke-width", "1") + line.setAttribute("stroke", color) + line.setAttribute("opacity", str(opacity)) + label.setAttribute("text-align", "middle") + label.setAttribute("fill", color) + label.setAttribute("font-size", "8px") + self.reference_group.appendChild(line) + self.reference_group.appendChild(label) + self.plot.appendChild(self.reference_group) + + # Creates legend for plot + def create_legend(self): + if not self.hide_legend: + legend = document.createElement('g') + legend.setAttribute("transform", "translate(" + str(self.width - self.margins.get("right") + 25) + " " + str(self.margins.get("top")) + ")") + i = 0 + for composite in self.composites: + # Creates legend entries for each composite + legend_element = document.createElement("g") + legend_element.setAttribute("transform", "translate(0," + str(24 * i) + ")") + legend_color_sense = document.createElement("polygon") + legend_color_sense.setAttribute("points", "0,0 15,0 15,15 0,15") + legend_color_sense.setAttribute("fill", composite.color) + legend_element.appendChild(legend_color_sense) + legend_color_anti = document.createElement("polygon") + legend_color_anti.setAttribute("points", "0,0 15,0 15,15 0,15") + legend_element.appendChild(legend_color_anti) + legend_color_anti.setAttribute("fill", composite.secondary_color) + legend_border = document.createElement("rect") + legend_border.setAttribute("width", "15") + legend_border.setAttribute("height", "15") + legend_border.setAttribute("stroke", "#000000") + legend_border.setAttribute("fill", "none") + legend_element.appendChild(legend_border) + id = document.createElement("text") + id.setAttribute("x", "20") + id.setAttribute("y", "10") + id.setAttribute("font-size", "10") + id.appendChild(document.createTextNode(str(composite.name))) + legend_element.appendChild(id) + legend.appendChild(legend_element) + i += 1 + self.plot.appendChild(legend) + + # Returns svg group with all composites and reference lines + def get_plot(self): + return self.plot + + # Exports json of all composites and plot attributes + def export(self): + composite_arr = [] + for composite in self.composites: + composite_arr.append({ + 'name': composite.name, + 'xmin': composite.xmin, + 'xmax': composite.xmax, + 'sense': composite.sense, + 'anti': composite.anti, + 'color': composite.color, + 'secondary-color': composite.secondary_color, + 'scale': composite.scale, + 'opacity': composite.opacity, + 'smoothing': composite.smoothing, + 'bp_shift': composite.bp_shift, + 'hide_sense': composite.hide_sense, + 'hide_anti': composite.hide_anti, + 'files_loaded': composite.files_loaded + }) + return { + 'settings' :composite_arr, + 'plot' : {'title': self.title, 'xlabel': self.xlabel, 'ylabel': self.ylabel, 'opacity': self.opacity, + 'smoothing': self.smoothing, 'bp_shift': self.bp_shift, 'xmin': self.xmin, 'xmax': self.xmax, 'ymin': self.ymin, + 'ymax': self.ymax, 'combined': self.combined, 'color_trace': self.color_trace, 'hide_legend': self.hide_legend} + } + + # Imports JSON with plot attributes and composites if desired. Preserves plot options specified by most recent call + def import_data(self, file, args, import_composites): + with open(file) as f: + data = json.load(f) + if import_composites: + for c in data['settings']: + n = c.get('name') + # Add _imported to composite name if duplicate of existing composite + if any(n == self.composites[j].name for j in range(len(self.composites))): + n = str(n) + "_imported" + self.composites.append(composite.Composite(scale=float(c.get('scale')) if c.get('scale') is not None else None, + color=c.get('color'), + secondary_color=c.get('secondary_color'), + opacity=c.get('opacity') if c.get('smoothing') != False else None, + smoothing=c.get('smoothing') if c.get('smoothing') != False else None, + bp_shift=c.get('bp_shift') if c.get('bp_shift') != False else None, + hide_sense=hide if (hide := c.get('hide')) == True else c.get('hide_forward'), + hide_anti=hide if hide == True else c.get('hide_reverse'), + baseline=c.get('baseline'), + name=n, + sense=c.get('sense'), + anti=c.get('anti'), + xmin=c.get('xmin'), + xmax=c.get('xmax'))) + plot_data = data['plot'] + # Add plot variables + self.title = plot_data.get('title', self.title) if args.title is None else self.title + self.xmin = plot_data.get('xmin', self.xmin) if args.xmin is None else self.xmin + self.xmax = plot_data.get('xmax', self.xmax) if args.xmax is None else self.xmax + self.ymin = plot_data.get('ymin', self.ymin) if args.ymin is None else self.ymin + self.ymax = plot_data.get('ymax', self.ymax) if args.ymax is None else self.ymax + self.xlabel = plot_data.get('xlabel', self.xlabel) if args.xlabel is None else self.xlabel + self.ylabel = plot_data.get('ylabel', self.ylabel) if args.ylabel is None else self.ylabel + self.opacity = plot_data.get('opacity', self.opacity) if args.opacity is None else self.opacity + self.smoothing = plot_data.get('smoothing', self.smoothing) if args.smoothing is None else self.smoothing + self.bp_shift = plot_data.get('bp_shift', self.bp_shift) if args.bp_shift is None else self.bp_shift + self.combined = plot_data.get('combined', self.combined) if args.combined is None else self.combined + self.color_trace = plot_data.get('color_trace', self.color_trace) if args.color_trace is None else self.color_trace + self.hide_legend = plot_data.get('hide_legend', self.hide_legend) if args.hide_legend is None else self.hide_legend + self.xscale = XScale(self) + self.yscale = YScale(self) + + def generateGradients(self, opacity, i, color, secondary_color=None): + # Creates DOM elements for top and bottom gradients + secondary_color = secondary_color if secondary_color is not None else color + # Generates top gradient + composite_gradient_top = document.createElement("linearGradient") + composite_gradient_top.setAttribute("class", "composite-gradient-top") + composite_gradient_top.setAttribute("x1", "0%") + composite_gradient_top.setAttribute("x2", "0%") + composite_gradient_top.setAttribute("y1", "0%") + composite_gradient_top.setAttribute("y2", "100%") + composite_gradient_top.setAttribute("id", "composite-gradient-top" + str(i)) + top_stop_one = document.createElement("stop") + top_stop_one.setAttribute("offset", "0") + top_stop_one.setAttribute("stop-color", color) + top_stop_one.setAttribute("stop-opacity", str(opacity)) + top_stop_two = document.createElement("stop") + top_stop_two.setAttribute("offset", "1") + top_stop_two.setAttribute("stop-color", color) + top_stop_two.setAttribute("stop-opacity", "0") + composite_gradient_top.appendChild(top_stop_one) + composite_gradient_top.appendChild(top_stop_two) + self.gradients_group.appendChild(composite_gradient_top) + # Generates bottom gradient + composite_gradient_bottom = document.createElement("linearGradient") + composite_gradient_bottom.setAttribute("class", "composite-gradient-bottom") + composite_gradient_bottom.setAttribute("x1", "0%") + composite_gradient_bottom.setAttribute("x2", "0%") + composite_gradient_bottom.setAttribute("y1", "100%") + composite_gradient_bottom.setAttribute("y2", "0%") + composite_gradient_bottom.setAttribute("id", "composite-gradient-bottom" + str(i)) + bottom_stop_one = document.createElement("stop") + bottom_stop_one.setAttribute("offset", "0") + bottom_stop_one.setAttribute("stop-color", secondary_color) + bottom_stop_one.setAttribute("stop-opacity", str(opacity)) + bottom_stop_two = document.createElement("stop") + bottom_stop_two.setAttribute("offset", "1") + bottom_stop_two.setAttribute("stop-color", secondary_color) + bottom_stop_two.setAttribute("stop-opacity", "0") + composite_gradient_bottom.appendChild(bottom_stop_one) + composite_gradient_bottom.appendChild(bottom_stop_two) + self.gradients_group.appendChild(composite_gradient_bottom) + +# Class that mimics d3 scaleLinear() for x-axis of plot +class XScale: + def __init__(self, plot): + self.plot = plot + self.domain = [plot.xmin, plot.xmax, plot.xmax - plot.xmin] + self.range = [plot.margins.get('left'), plot.width - plot.margins.get('right'), plot.width - (plot.margins.get('right') + plot.margins.get('left'))] + self.zero = (plot.width - (plot.margins.get('right') + plot.margins.get('left'))) * (abs(plot.xmin) / (abs(plot.xmin) + abs(plot.xmax))) + plot.margins.get('left') + # Returns position given bp + def get(self, value): + return (self.range[2] / self.domain[2]) * value + self.zero + # Returns bp given position + def inverse(self, value): + return (value - self.zero) * (self.domain[2] / self.range[2]) +# Class that mimics d3 scaleLinear() for y-axis of plot +class YScale: + def __init__(self, plot): + self.domain = [plot.ymin, plot.ymax, abs(plot.ymax) + abs(plot.ymin)] + self.range = [plot.margins.get('top'), plot.height - plot.margins.get('bottom'), plot.height - (plot.margins.get('top') + plot.margins.get('bottom'))] + self.zero = (plot.height - (plot.margins.get('top') + plot.margins.get('bottom'))) * (0.5) + plot.margins.get('top') if plot.combined is False else self.range[1] + # Returns position on svg given occupancy + def get(self, value): + return self.zero - (self.range[2] / self.domain[2]) * value + # Returns occupancy given position + def inverse(self, value): + return (value - self.zero) * (self.domain[2] / self.range[2]) diff --git a/python/plotter.py b/python/plotter.py new file mode 100644 index 0000000..0bf5d6b --- /dev/null +++ b/python/plotter.py @@ -0,0 +1,149 @@ +import plot +import composite +from composite import Composite +from composite import SimpleComposite +import re +import xml.dom.minidom as dom +import argparse +import math +import parseComposite +import sys +from enum import Enum +import svgFactory +import json + +document = dom.Document() + +def main(): + return True + +if __name__ == "__main__": + # Remove 'plotter' from sys.argv + sys.argv.pop(0) + # Load subcommands into appropriate arrays + i = -1 + k = -1 + composite_commands = [] + ref_line_commands = [] + plot_command = "" + current = "" + for word in sys.argv: + if word == "composite": + i += 1 + composite_commands.append("") + current = "composite" + elif word == "reference-line": + k += 1 + ref_line_commands.append("") + current = "ref" + elif word == "plot": + current = "plot" + elif current == "composite": + composite_commands[i] += f" {word}" + elif current == "ref": + ref_line_commands[k] += f" {word}" + elif current == "plot": + plot_command += f" {word}" + # Create parser for plot subcommand + plot_parser = argparse.ArgumentParser() + plot_parser.add_argument("--smoothing", type=int) + plot_parser.add_argument("--bp-shift", type=int) + plot_parser.add_argument("--opacity", type=float) + plot_parser.add_argument("--title", nargs="+") + plot_parser.add_argument("--xmin",type=int) + plot_parser.add_argument("--xmax",type=int) + plot_parser.add_argument("--xlabel", nargs="+") + plot_parser.add_argument("--ymin", type=int) + plot_parser.add_argument("--ymax", type=int) + plot_parser.add_argument("--ylabel", nargs="+") + plot_parser.add_argument("--color-trace", action="store_true", default=False) + plot_parser.add_argument("--combined", action="store_true", default=False) + plot_parser.add_argument("--hide-legend", action="store_true", default=False) + plot_parser.add_argument("--no-resize", action="store_true", default=False) + plot_parser.add_argument("--no-shrink", action="store_true", default=False) + plot_parser.add_argument("--out") + plot_parser.add_argument("--export-json") + plot_parser.add_argument("--import-json") + plot_parser.add_argument("--import-settings-json") + + # Create plot based on plot subcommand, default values in Plot class will be used if argument is not specified + plot_args = plot_parser.parse_args(plot_command.split()) + p = plot.Plot(title=" ".join(plot_args.title) if plot_args.title is not None else None, xmin=plot_args.xmin, xmax=plot_args.xmax, ymin=plot_args.ymin, ymax=plot_args.ymax, xlabel=" ".join(plot_args.xlabel) if plot_args.xlabel is not None else None, + ylabel=" ".join(plot_args.ylabel) if plot_args.ylabel is not None else None, opacity=plot_args.opacity, smoothing=plot_args.smoothing, bp_shift=plot_args.bp_shift, combined=plot_args.combined, color_trace=plot_args.color_trace, hide_legend=plot_args.hide_legend) + + # Create arrays for default composite names and colors + names = range(1, len(composite_commands) + 1) + colors = ["#BFBFBF","#000000","#FF0000","#FF9100","#D7D700","#07E200","#00B0F0","#0007FF","#A700FF","#FF00D0"] + # Create parser for composite subcommands + composite_parser = argparse.ArgumentParser() + composite_parser.add_argument("files") + composite_parser.add_argument("--name") + composite_parser.add_argument("--color") + composite_parser.add_argument("--secondary-color") + composite_parser.add_argument("--scale", type=float) + composite_parser.add_argument("--shift-occupancy", type=float) + composite_parser.add_argument("--smoothing", type=int) + composite_parser.add_argument("--opacity", type=float) + composite_parser.add_argument("--bp-shift", type=int) + composite_parser.add_argument("--hide-sense", action="store_true", default=False) + composite_parser.add_argument("--hide-anti", action="store_true", default=False) + composite_parser.add_argument("--swap-strands", action="store_true", default=False) + # Parse composite subcommands, use values values in Composite class if not specified + i = 0 + for command in composite_commands: + args = composite_parser.parse_args(command.split()) + composite = Composite(scale=args.scale, color=args.color if args.color is not None else colors[i % len(colors)], secondary_color=args.secondary_color, + smoothing=args.smoothing, bp_shift=args.bp_shift, hide_sense= args.hide_sense, hide_anti= args.hide_anti, baseline=args.shift_occupancy, + name=args.name if args.name is not None else names[i], opacity=args.opacity,) + + composite_files = args.files.split(":") + for c in composite_files: + #Check if composite file contains multiple composites + if sum(1 for line in open(c) if len(line.strip()) != 0) <= 3: + sc = parseComposite.parse_simple(c) + composite.load_simple_composite(sc) + else: + prefixes = parseComposite.get_prefixes_from_multiple_composites(c) + cd = parseComposite.parse_multiple_composite(c, prefixes[0]) + composite.load_composite_dict(cd) + p.add_composite_group(composite) + i += 1 + + # Import settings and composites from plot, preserving options specified in this call + if plot_args.import_json: + p.import_data(plot_args.import_json, plot_args, True) + elif plot_args.import_settings_json: + p.import_data(plot_args.import_settings_json, plot_args, False) + + # If --no-shrink is specified, don't change y-axis but resize x-axis + if plot_args.no_shrink: + p.autoscale_axes(False) + # If --no-resize is specified, don't change either axis + elif not plot_args.no_resize: + p.autoscale_axes(True) + + p.plot_composites() + + # Create parser for reference-line subcommand + reference_parser = argparse.ArgumentParser() + reference_parser.add_argument("axis") + reference_parser.add_argument("--style") + reference_parser.add_argument("--color") + reference_parser.add_argument("--val", type=float) + reference_parser.add_argument("--opacity",type=float) + # Add reference lines to plot + for command in ref_line_commands: + args = reference_parser.parse_args(command.split()) + p.plot_reference_line(axis=args.axis, val=args.val, style=args.style, color=args.color, opacity=args.opacity) + # Use svg factory to generate svg based on plot + svg = svgFactory.generateSVG(p) + with open(plot_args.out if plot_args.out is not None else "out.svg", 'w') as f: + # write to output + svg.writexml(f, addindent=' ', newl='\n') + + # Output plot json if specified + if plot_args.export_json: + str = json.dumps(p.export(), indent=2) + with open(plot_args.export_json, 'w') as f: + # Write to output + f.write(str) \ No newline at end of file diff --git a/python/readme.md b/python/readme.md new file mode 100644 index 0000000..5da10d4 --- /dev/null +++ b/python/readme.md @@ -0,0 +1,94 @@ +## Python Plotter + +This is a Python implementation of a plotter that creates plot-esque plots based on subcommands passed into `plotter.py`. + +The three basic subcommands are `composite`, `plot`, and `reference-line`. They're passed into `plotter.py` using a single line with the basic syntax: + +``` +python plotter.py composite [file] [composite options] reference-line [axis] [line options] plot [plot options] +``` + +An example command is: + +``` +python plotter.py composite sample.out --bp-shift 50 composite sample2.out --bp-shift 50 reference-line y --color blue plot --title "Samples One and Two" --smoothing 10 --out out.svg +``` + +The `composite` and `reference-line` subcommands can be repeated for as many composites or reference lines as you wish to add, but the `plot` command should only be used once per call. If `--opacity`, `--smoothing`, or `--bp-shift` is specified for a composite, those specifications will override default values inherited by values specified in the plot subcommand. Also, all boolean options are flags; if not specified they will remain the default value. + +### Plot + +``` +plot [plot options] +``` + +The `plot` subcommand takes no positional arguments, and the options specify properties for the entire plot, such as domain, range, and the axis labels. Options can also be used to specify default properties for all composites such as `opacity` and `smoothing`. This implementation of the plotter autoscales the axes to fit the largest composite by default, ignoring the `xmin`, `xmax`, `ymin`, and `ymax` options unless `--no-shrink` or `--no-resize` is specified. + +The available options for the `plot` subcommand are: + +| Command | Type | Description | Default | +| ---------------- | ------- | ---------------------------------------- | --------------- | +| --smoothing | Float | The default smoothing value | 7 | +| --bp-shift | Int | The default bp-shift | 0 | +| --opacity | Float | The default maximum opacity | 1 | +| --title | String | The title of the plot | "Composite plot" | +| --xmin | Int | Minimum value of the plot | -500 | +| --xmax | Int | Maximum value of the plot | 500 | +| --ymin | Int | Minimum occupancy of the plot | -1 | +| --ymax | Int | Maximum occupancy of the plot | 1 | +| --ylabel | String | Label for the y-axis | Occupancy (AU) | +| --xlabel | String | Label for the x-axis | Position (bp) | +| --color-trace | Boolean | Default for if composites should have composite trace | False | +| --combined | Boolean | Draws a combined plot | False | +| --hide-legend | Boolean | Hides the plot legend | False | +| --no-resize | Boolean | Prevents plotter from autoscaling the x and y axes | False | +| --no-shrink | Boolean | Prevents plotter from autoscaling the y-axis | False | +| --out | String | Name and filepath of svg output | `out.svg` | +| --export-json | String | JSON file to export composites and plot settings | `None` | +| --import-json | String | JSON file to import composites and plot settings | `None` | +| --import-settings-json | String | JSON file to import plot settings | `None` | + +### Composite + +``` +composite [file] [composite options] +``` + +The `composite` subcommand takes a positional argument that specifies the files to be loaded into the composite. If there are multiple files, they should be separated by a ":". For example, `composite sample1.out-sample2.out-sample3.out --opacity 0.5` loads samples 1-3 into the composite and sets the maximum opacity to 0.5. + +The available options for the `composite` subcommand are: + +| Command | Type | Description | Default | +| ----------------- | ------- | ---------------------------------------- | --------------- | +| --name | String | Assigns the name of the composite for the plot's legend | None | +| --color | String | Sets the color of the composite using hex code or default HTML color | Plotter Defaults | +| --secondary-color | String | Sets the secondary color of the composite using hex code or default HTML color | The primary color | +| --scale | Float | Sets the scale of the composite | 1 | +| --shift-occupancy | Float | Shifts occupancy by a set value | 0 | +| --smoothing | Float | Sets smoothing | 7 | +| --opacity | Float | Sets opacity | 1 | +| --bp-shift | Int | Sets bp shift | 0 | +| --hide-sense | Boolean | Hides the sense strand | False | +| --hide-anti | Boolean | Hides the anti strand | False | +| --swap-strands | Boolean | Swaps the sense and anti strands | False | +| --export-json | String | Path to export json with composites and plot values| None | +| --import-json | String | Path to import json with composites and plot values | None | + +### Reference-Line + +``` +reference-line [axis] [reference-line options] +``` + +The `reference-line` subcommand takes either `x` or `y` to specify the axis it should be plotted on followed by options specifying its attributes. + +The available options for the `reference-line` subcommand are: + +| Command | Type | Description | Default | +| ---------------- | ------- | ---------------------------------------- | --------------- | +| --val | Float | Sets the position of the line (bp if x-axis or AU if y-axis) | 0 | +| --color | String | Sets the color of the line using hex code or default HTML color | Plotter Defaults | +| --style | `dashed`, `dotted`, or `solid` | Sets the style of the line | `dashed` | +| --opacity | Float | Sets the opacity | 1 | + +--- \ No newline at end of file diff --git a/python/slidingWindow.py b/python/slidingWindow.py new file mode 100644 index 0000000..99c287b --- /dev/null +++ b/python/slidingWindow.py @@ -0,0 +1,11 @@ +# Copy of sliding_window from plotter +def sliding_window(xdomain, occupancy, window): + occupancy_val = sum(occupancy[:window]) / window + + new_xdomain = [(xdomain[0] + xdomain[window - 1]) / 2] + new_occupancy = [occupancy_val] + for i in range(len(occupancy) - window - 1): + new_xdomain.append((xdomain[i + 1] + xdomain[i + window]) / 2) # Added parentheses here + occupancy_val += (occupancy[i + window] - occupancy[i]) / window + new_occupancy.append(occupancy_val) + return {"new_xdomain": new_xdomain, "new_occupancy": new_occupancy} \ No newline at end of file diff --git a/python/svgFactory.py b/python/svgFactory.py new file mode 100644 index 0000000..b35d332 --- /dev/null +++ b/python/svgFactory.py @@ -0,0 +1,214 @@ +import plot +import composite +from composite import Composite +from composite import SimpleComposite +import re +import xml.dom.minidom as dom +import argparse +import math +import parseComposite +import sys +from enum import Enum + +document = dom.Document() + +def generateSVG(plot): + # Create svg with similar attributes to plotter + svg = document.appendChild(document.createElement('svg')) + svg.setAttribute("xmlns", "http://www.w3.org/2000/svg") + svg.setAttribute("id", "main-plot") + svg.setAttribute("font-family", "Helvetica") + svg.setAttribute("viewBox", "0 0 460 300") + svg.setAttribute("style", "height: 50vh; max-width: 100%; overflow: hide;") + svg.setAttribute("baseProfile", "full") + # Create title + title = document.createElement('text') + title.setAttribute("font-size", "16") + title.setAttribute("x", str((plot.width + plot.margins.get('left') - plot.margins.get('right')) / 2)) + title.setAttribute("y", "20") + title.setAttribute("label", "title") + title.setAttribute("id", "main-plot-title") + title.setAttribute("style", "text-anchor: middle; cursor: pointer;") + title.appendChild(document.createTextNode(plot.title)) + # Create xlabel, xmin and xmax + xlabel = document.createElement('text') + xlabel.setAttribute("font-size", "16") + xlabel.setAttribute("x", str((plot.width + plot.margins.get('left') - plot.margins.get('right')) / 2)) + xlabel.setAttribute("y", str(plot.height - 5)) + xlabel.setAttribute("label", "xlabel") + xlabel.setAttribute("id", "main-plot-xlabel") + xlabel.setAttribute("style", "text-anchor: middle; cursor: pointer;") + xlabel.appendChild(document.createTextNode(plot.xlabel)) + xmin = document.createElement('text') + xmin.setAttribute("x", str(plot.margins.get("left"))) + xmin.setAttribute("y", str(plot.height - plot.margins.get("bottom") + 15)) + xmin.setAttribute("text-anchor", "middle") + xmin.setAttribute("font-size", "14px") + xmin.appendChild(document.createTextNode(str(plot.xmin))) + xmax = document.createElement('text') + xmax.setAttribute("x", str(plot.width - plot.margins.get("right"))) + xmax.setAttribute("y", str(plot.height - plot.margins.get("bottom") + 15)) + xmax.setAttribute("text-anchor", "middle") + xmax.setAttribute("font-size", "14px") + xmax.appendChild(document.createTextNode(str(plot.xmax))) + # Create ylabel with exponent + ylabel = document.createElement('text') + ylabel.setAttribute("font-size", "16") + ylabel.setAttribute("x", "12") + ylabel.setAttribute("y", str((plot.height + plot.margins.get('top') - plot.margins.get('bottom')) / 2)) + ylabel.setAttribute("label", "ylabel") + ylabel.setAttribute("id", "main-plot-ylabel") + ylabel.setAttribute("transform", "rotate(-90 12 147.5)") + ylabel.setAttribute("style", "text-anchor: middle; cursor: pointer;") + round_exp = 1 - math.floor(math.log10(plot.ymax - plot.ymin)) + round_factor = 10 ** round_exp + exp_label = round_exp <= -2 or round_exp >= 2 + if exp_label: + ylabel.appendChild(document.createTextNode(plot.ylabel + " X10")) + ylabel_suffix = document.createElement("tspan") + ylabel_suffix.setAttribute("font-size", "8px") + ylabel_suffix.setAttribute("baseline-shift", "super") + ylabel_suffix.appendChild(document.createTextNode(str(1 - round_exp))) + ylabel.appendChild(ylabel_suffix) + else: + ylabel.appendChild(document.createTextNode(plot.ylabel)) + # Create ymin and ymax + if not plot.combined: + ymin = document.createElement('text') + ymin.setAttribute("x", "30") + ymin.setAttribute("y", str(plot.height - plot.margins.get("bottom"))) + ymin.setAttribute("text-anchor", "end") + ymin.setAttribute("font-size", "14px") + ymin.appendChild(document.createTextNode(str((round(plot.ymin, 2) * round_factor) / (10 if exp_label else round_factor)))) + svg.appendChild(ymin) + ymax = document.createElement('text') + ymax.setAttribute("x", "30") + ymax.setAttribute("y", str(plot.margins.get("top") + 10)) + ymax.setAttribute("text-anchor", "end") + ymax.setAttribute("font-size", "14px") + ymax.appendChild(document.createTextNode(str((round(plot.ymax, 2) * round_factor) / (10 if exp_label else round_factor)))) + # Create vertical line at reference point + zero_line = document.createElement("line") + zero_line.setAttribute("stroke", "grey") + zero_line.setAttribute("opacity", "0.5") + zero_line.setAttribute("y1", str(plot.margins.get("top"))) + zero_line.setAttribute("y2", str(plot.height - plot.margins.get("bottom"))) + zero_line.setAttribute("x1", str(plot.xscale.get(0))) + zero_line.setAttribute("x2", str(plot.xscale.get(0))) + zero_line.setAttribute("stroke-dasharray", "5,5") + svg.appendChild(zero_line) + svg.appendChild(title) + svg.appendChild(xlabel) + svg.appendChild(ylabel) + svg.appendChild(xmin) + svg.appendChild(xmax) + svg.appendChild(ymax) + # Append composites, reference lines, and legend from plot onto svg + plot.create_legend() + svg.appendChild(plot.get_plot()) + # Create axes with tick marks + axis_left = axis("left", None, plot) + axis_right = axis("right", None, plot) + axis_bottom = axis("bottom", None, plot) + axis_top = axis("top", None, plot) + axis_middle = axis("middle", None, plot) + # Append all elements to svg + svg.appendChild(axis_left) + svg.appendChild(axis_right) + svg.appendChild(axis_bottom) + svg.appendChild(axis_top) + svg.appendChild(axis_middle) + # Return the svg + return svg + +# Create axis elements for plot +def axis(orient, scale, plot): + # Use appropriate tick parameters for axis + tickSpacing = 23.5 if orient == "left" or orient == "right" else 25 + tickSize = 6 if orient == "left" or orient == "top" else -6 + axis_group = document.createElement("g") + axis = document.createElement("line") + # Get coords for plot margins + bottom = plot.height - plot.margins.get('bottom') + top = plot.margins.get("top") + right = plot.width - (plot.margins.get('right')) + left = plot.margins.get("left") + # Draw left axis + if (orient == "left"): + axis.setAttribute("x1", str(left)) + axis.setAttribute("x2", str(left)) + axis.setAttribute("y1", str(top)) + axis.setAttribute("y2", str(bottom)) + i = top + while i < bottom: + tick = document.createElement("line") + tick.setAttribute("y1", str(i)) + tick.setAttribute("y2", str(i)) + tick.setAttribute("x1", str(left)) + tick.setAttribute("x2", str(left + tickSize)) + axis_group.appendChild(tick) + i += tickSpacing + # Draw right axis + elif (orient == "right"): + axis.setAttribute("x1", str(right)) + axis.setAttribute("x2", str(right)) + axis.setAttribute("y1", str(top)) + axis.setAttribute("y2", str(bottom)) + i = top + while i < bottom: + tick = document.createElement("line") + tick.setAttribute("y1", str(i)) + tick.setAttribute("y2", str(i)) + tick.setAttribute("x1", str(right)) + tick.setAttribute("x2", str(right + tickSize)) + axis_group.appendChild(tick) + i += tickSpacing + # Draw bottom axis + elif(orient == "bottom"): + axis.setAttribute("x1", str(left)) + axis.setAttribute("x2", str(right)) + axis.setAttribute("y1", str(bottom)) + axis.setAttribute("y2", str(bottom)) + i = left + while i < right: + tick = document.createElement("line") + tick.setAttribute("y1", str(bottom)) + tick.setAttribute("y2", str(bottom + tickSize)) + tick.setAttribute("x1", str(i)) + tick.setAttribute("x2", str(i)) + axis_group.appendChild(tick) + i += tickSpacing + # Draw top axis + elif (orient == "top"): + axis.setAttribute("x1", str(left)) + axis.setAttribute("x2", str(right)) + axis.setAttribute("y1", str(top)) + axis.setAttribute("y2", str(top)) + i = left + while i < right: + tick = document.createElement("line") + tick.setAttribute("y1", str(top)) + tick.setAttribute("y2", str(top + tickSize)) + tick.setAttribute("x1", str(i)) + tick.setAttribute("x2", str(i)) + axis_group.appendChild(tick) + i += tickSpacing + # Draw middle axis if plot is not combined + elif (orient == "middle"): + if not plot.combined == True: + axis.setAttribute("x1", str(left)) + axis.setAttribute("x2", str(right)) + axis.setAttribute("y1", str(plot.yscale.get(0))) + axis.setAttribute("y2", str(plot.yscale.get(0))) + i = left + while i < right: + tick = document.createElement("line") + tick.setAttribute("y1", str(plot.yscale.get(0) - tickSize)) + tick.setAttribute("y2", str(plot.yscale.get(0) + tickSize)) + tick.setAttribute("x1", str(i)) + tick.setAttribute("x2", str(i)) + axis_group.appendChild(tick) + i += tickSpacing + axis_group.setAttribute("stroke", "black") + axis_group.appendChild(axis) + return axis_group \ No newline at end of file