diff --git a/README.md b/README.md index 3bc6b38..652b3c4 100644 --- a/README.md +++ b/README.md @@ -326,7 +326,7 @@ And a more detailed tutorial could be found at [here.](https://trackplot.readthe ```bash # example of basic plot types -python main.py \ +python ../main.py \ -e chr1:1270656-1284730:+ \ -r example/example.sorted.gtf.gz \ --interval example/interval_list.tsv \ @@ -345,6 +345,7 @@ python main.py \ --barcode example/barcode_list.tsv \ --domain --remove-duplicate-umi \ --normalize-format cpm \ + --annotation-scale .3 \ -p 4 ``` @@ -377,6 +378,7 @@ docker run -v $PWD:$PWD --rm ygidtu/trackplot \ --barcode $PWD/example/barcode_list.tsv \ --domain --remove-duplicate-umi \ --normalize-format cpm \ + --annotation-scale .3 \ -p 4 ``` diff --git a/cmdAppImageBuilder.yml b/cmdAppImageBuilder.yml index f18dbfe..cf3054f 100644 --- a/cmdAppImageBuilder.yml +++ b/cmdAppImageBuilder.yml @@ -11,7 +11,7 @@ AppDir: app_info: id: org.appimage-crafters.trackplot name: trackplot - version: 0.2.7 + version: 0.2.8 # Set the python executable as entry point exec: "bin/python3" # Set the application main script path as argument. Use '$@' to forward CLI parameters diff --git a/example/example.png b/example/example.png index 758a26a..07d824b 100644 Binary files a/example/example.png and b/example/example.png differ diff --git a/main.py b/main.py index 640c836..665e36c 100644 --- a/main.py +++ b/main.py @@ -9,5 +9,7 @@ if __name__ == "__main__": - main() - + try: + main() + except Exception as err: + logger.exception(err) diff --git a/pyproject.toml b/pyproject.toml index f107eba..328c03b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "trackplot" -version = "0.2.7" +version = "0.2.8" description = "The trackplot is a tool for visualizing various next-generation sequencing (NGS) data, including DNA-seq, RNA-seq, single-cell RNA-seq and full-length sequencing datasets. https://sashimi.readthedocs.io/" authors = ["ygidtu "] license = "BSD-3" diff --git a/server.py b/server.py index 07ab28e..50750f0 100644 --- a/server.py +++ b/server.py @@ -8,18 +8,28 @@ from glob import glob import click - from flask import Flask, render_template, jsonify, send_file, request from trackplot.cli import load_barcodes, __version__ +from trackplot.conf.config import COLORMAP from trackplot.plot import * + __DIR__ = os.path.abspath(os.path.dirname(__file__)) __UI__ = os.path.join(__DIR__, "ui") __PLOT__ = os.path.join(os.path.dirname(__file__), "plots") -app = Flask( __name__, static_url_path="/static", static_folder=__UI__, template_folder=__UI__) +app = Flask(__name__, static_url_path="/static", static_folder=__UI__, template_folder=__UI__) + +__SUPPORT_FORMAT__ = { + "add_density": ['bam', 'bigwig', 'bedgraph', 'depth', 'atac'], + "add_line": ['bam', 'bigwig', 'bedgraph', 'depth', 'atac'], + "add_heatmap": ['bam', 'bigwig', 'bedgraph', 'depth', 'atac'], + "add_igv": ['bam', 'bed'], + "add_hic": ['h5'], + +} # supported trackplot settings __COMMON_PARAMS__ = [ @@ -31,9 +41,9 @@ }, { "key": "category", - "annotation": "str", + "annotation": "choice", "default": "bam", - "note": "The category of input file" + "note": "The input file category can be selected from options such as BAM, ATAC, IGV, Hi-C, BigWig/BW, BedGraph/BG, and Depth." }, { "key": "size_factor", @@ -63,13 +73,13 @@ "key": "library", "annotation": "choice['frr', 'frs', 'fru']", "default": "fru", - "note": "The strand preference of input file, frf => fr-firststrand; frs => fr-secondstrand; fru => fr-unstrand" + "note": "The strand pannotation of input file, frf => fr-firststrand; frs => fr-secondstrand; fru => fr-unstrand" }, { "key": "n_y_ticks", "annotation": "int", "default": "4", - "note": "Number of y ticks" + "note": "Set the number of ticks for the y-axis." }, { "key": "show_y_label", @@ -87,7 +97,7 @@ "key": "color", "annotation": "color", "default": "#409EFF", - "note": "The fill color of density" + "note": "The color of the current track." }, { "key": "font_size", @@ -99,19 +109,17 @@ "key": "log_trans", "annotation": "choice['0', '2', '10']", "default": "0", - "note": "Whether to perform log transformation, 0 -> not log transform;2 -> log2;10 -> log10" + "note": "Choose whether to perform a log transformation for coverage. Use '0' for no transformation, '2' for log2 transformation, and '10' for log10 transformation." + }, + { + "key": "group", + "annotation": "str", + "default": "default", + "note": "The group name of heatmap/line track" }, ] __PARAMS__ = { - "add_customized_junctions": [ - { - "key": "path", - "annotation": "str", - "default": "", - "note": "Path to junction table column name needs to be bam name or bam alias." - } - ], "add_density": [ [ "path", "category", "size_factor", "label", "title", @@ -123,37 +131,31 @@ "key": "density_by_strand", "annotation": "bool", "default": "false", - "note": "Whether draw density plot by strand" + "note": "Whether draw density plot in a strand-aware manner. If enabled, the tool will consider the library parameter (frf or frs) to perform strand-aware coverage calculation." }, { "key": "show_junction_number", "annotation": "bool", "default": "true", - "note": "Whether to show junction number" + "note": "Whether to show the depth of each splicing junction. If activated, the tool will display the depth of each splicing junction which will be visualized at the midpoint of the junction span line." }, { "key": "junction_number_font_size", "annotation": "int", "default": "5", - "note": "The font size of junction number" + "note": "The font size of the depth of splicing junction." }, { "key": "show_site_plot", "annotation": "bool", "default": "false", - "note": "Whether to show site plot" + "note": "Whether to include a site coverage plot for the current track as well. If enabled, only the most 3' end site of each read will be counted in the coverage matrix. When strand-aware mode is enabled, this allows for easy distinction of alternative polyadenylation events in 3'-tag sequencing datasets." }, { "key": "strand_choice", "annotation": "choice['all', '+', '-']", "default": "all", - "note": "Which strand kept for site plot, default use all" - }, - { - "key": "only_customized_junction", - "annotation": "bool", - "default": "false", - "note": "Only used customized junctions." + "note": "The coverage matrix used for generating the site plot can be specified by strand. By default, coverage from all strands is used. If the + or - option is enabled, only the coverage matrix from the chosen strand will be displayed." } ], "add_focus": [ @@ -161,7 +163,7 @@ "key": "start", "annotation": "int", "default": "0", - "note": "The start site of focus region" + "note": "The start site of focus region. More detailed instructions please refer to https://trackplot.readthedocs.io/en/latest/command/#additional-annotation" }, { "key": "end", @@ -174,57 +176,57 @@ ["path", "group", "category", "size_factor", "label", "title", "barcode_groups", "barcode_tag", "umi_tag", "library", "color", "font_size", "show_y_label", "log_trans"], - { - "key": "group", - "annotation": "str", - "default": "", - "note": "The heatmap group" - }, { "key": "do_scale", "annotation": "bool", "default": "false", - "note": "Whether to scale the matrix" + "note": "Whether perform a scale for the coverage matrix by samples." }, { "key": "clustering", "annotation": "bool", "default": "false", - "note": "Whether reorder matrix by clustering" + "note": "Whether perform a cluster analysis for the samples." }, { "key": "clustering_method", "annotation": "str", "default": "ward", - "note": "same as scipy.cluster.hierarchy.linkage" + "note": "If `clustering` enabled, user could select a clustering method for downstream analysis. More clustering method please refer to https://docs.scipy.org/doc/scipy/reference/generated/scipy.cluster.hierarchy.linkage.html ." }, { "key": "distance_metric", "annotation": "str", "default": "euclidean", - "note": "same as scipy.spatial.distance.pdist" + "note": "The method to perform pairwise distances analysis. More detail please refer to https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.distance.pdist.html ." }, { "key": "show_row_names", "annotation": "bool", "default": "false", - "note": "Whether to show row names" + "note": "Whether to add the filename as the row name of the heatmap." }, { "key": "vmin", "annotation": "float", "default": "", - "note": "Values to anchor the colormap, otherwise they are inferred from the data and other keyword arguments." + "note": "The parameter Vmin determines the minimum value used for the colormap. If not specified, the minimum value will be inferred from the data and other keyword arguments." }, { "key": "vmax", "annotation": "float", "default": "", - "note": "Values to anchor the colormap, otherwise they are inferred from the data and other keyword arguments." + "note": "The parameter Vmax determines the maximum value used for the colormap. If not specified, the maximum value will be inferred from the data and other keyword arguments." } ], "add_hic": [ - ["path", "category", "label", "color", "font_size", "n_y_ticks", "show_y_label"], + ["path", "category", "label", "font_size", "n_y_ticks", "show_y_label"], + { + "key": "color", + "annotation": f"select[{','.join(COLORMAP)}]", + "default": "RdYlBu_r", + "note": "The color of the current track." + }, { "key": "trans", "annotation": "Optional[str]", @@ -235,13 +237,13 @@ "key": "show_legend", "annotation": "bool", "default": "true", - "note": "Whether to show legend" + "note": "Whether to show the legend of each line." }, { "key": "depth", "annotation": "int", "default": "30000", - "note": "The default depth for HiCMatrix" + "note": "The length of region to visualize genomic interaction." }, ], "add_igv": [ @@ -256,31 +258,31 @@ "key": "deletion_ignore", "annotation": "Optional[int]", "default": "true", - "note": "Whether ignore the gap in full length sequencing data" + "note": "Whether to ignore deletion regions in full-length sequencing data can be specified. Full-length sequencing data, especially nanopore data, often contains a high ratio of deletions. By enabling this option, users can exclude these deletion regions from visualization." }, { "key": "del_ratio_ignore", "annotation": "float", "default": "0.5", - "note": "Ignore the deletion gap in nanopore or pacbio reads" + "note": "The threshold for ignoring deletions in full-length sequencing data. For example, if a deletion ignore ratio of 0.5 is selected and a 1000 bp isoform is captured in sequencing, deletions within 1000 * 0.5 length will be filled and not visualized." }, { "key": "exon_color", "annotation": "color", "default": "#000000", - "note": "The color of exons" + "note": "The color of exon structure in current track." }, { "key": "intron_color", "annotation": "color", "default": "#000000", - "note": "The color of introns" + "note": "The color of intron structure in current track." }, { "key": "exon_width", "annotation": "float", "default": "0.3", - "note": "The default width of exons" + "note": "The exon width of current track" } ], "add_interval": [ @@ -305,19 +307,19 @@ "key": "show_legend", "annotation": "bool", "default": "false", - "note": "Whether to show legend" + "note": "Whether to show the legend of each line." }, { "key": "legend_position", "annotation": "str", "default": "upper right", - "note": "The position of legend" + "note": "The position of the legend for current track, default: upper right. Other layout such as upper left, lower left, lower right, right, center left, center right, lower center, upper center, and center will place the legend at the corresponding corner of the track." }, { "key": "legend_ncol", "annotation": "int", "default": "0", - "note": "The number of columns of legend" + "note": "The number of columns for current legend." } ], "add_links": [ @@ -325,7 +327,7 @@ "key": "start", "annotation": "int", "default": "0", - "note": "The start site of link" + "note": "The start site of link line. If the \"Links\" option is enabled, a pseudo span line will be added at the bottom of the tracks. This can be used by users to indicate the back-splice junction of circular RNA (circRNA). A detailed instruction please refer to https://trackplot.readthedocs.io/en/latest/command/#circrna-plot ." }, { "key": "end", @@ -336,14 +338,14 @@ { "key": "label", "annotation": "str", - "default": "", + "default": "", "note": "The label of link" }, { "key": "color", "annotation": "color", "default": "#409EFF", - "note": "The color of link" + "note": "The color of link line." } ], "add_sites": [ @@ -351,7 +353,7 @@ "key": "sites", "annotation": "str", "default": "", - "note": "Where to plot additional indicator lines, comma separated int" + "note": "Where to plot additional indicator lines. The user is required to provide an integer number within the region of interest. If there are multiple lines, the integer numbers should be separated by commas." } ], "add_stroke": [ @@ -359,7 +361,7 @@ "key": "start", "annotation": "int", "default": "0", - "note": "The start site of stroke" + "note": "The start site of stroke. More detailed instructions please refer to https://trackplot.readthedocs.io/en/latest/command/#additional-annotation" }, { "key": "end", @@ -382,16 +384,16 @@ ], "plot": [ { - "key": "reference_scale", + "key": "annotation_scale", "annotation": "Union[int, float]", "default": "0.25", - "note": "The size of reference plot in final plot" + "note": "The height of annotation track, the height = (number of annotation) * annotation_scale" }, { "key": "stroke_scale", "annotation": "Union[int, float]", "default": "0.25", - "note": "The size of stroke plot in final image" + "note": "The height of stroke track, the height = (number of strokes) * stroke_scale" }, { "key": "dpi", @@ -403,37 +405,37 @@ "key": "width", "annotation": "Union[int, float]", "default": "10", - "note": "The width of output file, default adjust image width by content" + "note": "The width of output file" }, { "key": "height", "annotation": "Union[int, float]", "default": "1", - "note": "The height of single subplot, default adjust image height by content" + "note": "The height of each track, the igv/annotation/stroke tracks will adjust the height according to their scales" }, { "key": "raster", "annotation": "bool", "default": "false", - "note": "The would convert heatmap and site plot to raster image (speed up rendering and produce smaller files), only affects pdf, svg and PS" + "note": "Choose whether to convert the heatmap and site plot to a raster image format, which can speed up rendering and result in smaller file sizes. Note that this option only affects the output formats of PDF, SVG, and PS. If enabled, the resolution of the output will decrease." }, { "key": "distance_ratio", "annotation": "float", "default": "0", - "note": "The distance between transcript label and transcript line" + "note": "Adjust the distance between transcript label and transcript structure in the annotation track. And the distacne between track title and the track" }, { "key": "n_jobs", "annotation": "int", "default": "1", - "note": "How many cpu to use" + "note": "Number of cores to perform data preparation." }, { "key": "fill_step", "annotation": "str", "default": "post", - "note": "Define step if the filling should be a step function, i.e. constant in between x. The value determines where the step will occur:\n" + "note": "The step parameter of matplotlib.axes.Axes.fill_between. Define step if the filling should be a step function, i.e. constant in between x. The value determines where the step will occur:\n" "pre:The y value is continued constantly to the left from every x position, i.e. the interval (x[i-1], x[i]] has the value y[i].\n" "post:The y value is continued constantly to the right from every x position, i.e. the interval [x[i], x[i+1]) has the value y[i].\n" "mid:Steps occur half-way between the x positions." @@ -442,37 +444,19 @@ "key": "same_y", "annotation": "bool", "default": "false", - "note": "Whether different sashimi/line plots shared same y-axis boundaries" - }, - { - "key": "remove_duplicate_umi", - "annotation": "bool", - "default": "false", - "note": "Drop duplicated UMIs by barcode" + "note": "13.Whether share a same y-axis among all data tracks." }, { "key": "threshold", "annotation": "int", "default": "0", - "note": "Threshold to filter low abundance junctions" + "note": "The threshold for filter these splicing junction by depth." }, { "key": "normalize_format", "annotation": "choice['normal', 'cpm', 'rpkm']", "default": "normal", - "note": "The normalize format for bam file" - }, - { - "key": "fill_step", - "annotation": "choice['post', 'pre', 'mid']", - "default": "post", - "note": "Define step if the filling should be a step function, i.e. constant in between x. Detailed info please check: https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.fill_between.html" - }, - { - "key": "normalize_format", - "annotation": "choice['count', 'cpm', 'rpkm']", - "default": "count", - "note": "The normalize format for bam file" + "note": "The normalize format for bam file." }, { "key": "smooth_bin", @@ -481,30 +465,42 @@ "note": "The bin size used to smooth ATAC fragments." } ], - "set_reference": [ + "set_annotation": [ { "key": "gtf", "annotation": "str", "default": "", - "note": "Path to reference file, in sorted and gzipped gtf format" + "note": "Path to annotation file, in sorted and gzipped gtf format" }, { "key": "add_domain", "annotation": "bool", "default": "false", - "note": "Add domain information into reference track" + "note": "Whether add domain information into the annotation track at bottom. " + "When the parameter is activated, " + "Trackplot will initiate a request to the EBI and UNIPROT APIs to retrieve protein annotation. " + "The retrieved annotation is then converted into genomic coordinates, " + "and the resulting domain information is visualized in a manner similar to a transcript structure. " + "This can be seen in Supplementary Figure 2A." }, { "key": "remove_empty_transcripts", "annotation": "bool", "default": "false", - "note": "Whether show transcripts without any exons in target region" + "note": "Whether show transcripts without any exons in the target region. " + "Sometimes, " + "the region of interest may be located within a large intron of a transcript, " + "which contains less informative data. " + "To exclude these transcript structures and focus on the relevant regions, " + "users can activate this parameter." }, { "key": "choose_primary", "annotation": "bool", "default": "false", - "note": "Whether choose primary transcript to plot." + "note": "Whether choose primary transcript to plot. " + "Once the parameter is activated, " + "the tool will display the longest transcript of each gene within the region of interest." }, { "key": "color", @@ -516,43 +512,53 @@ "key": "font_size", "annotation": "int", "default": "5", - "note": "The font size of reference" + "note": "The font size of the annotation tracks." }, { "key": "no_gene", "annotation": "bool", "default": "false", - "note": "Do not show gene id next to transcript id" + "note": "Do not show gene id next to transcript id. " + "If set to false, the format \"isoform1|gene_id\" will be displayed. " + "If set to true, only the \"isoform1\" ID will be shown." }, { "key": "show_id", "annotation": "bool", "default": "false", - "note": "Show gene name or gene id" + "note": "Show gene name (ensemble ID) or gene id (gene symbol). " + "If set to false, an ensemble ID will be presented. " + "If set to true, a symbol ID will be shown." }, { "key": "show_exon_id", "annotation": "bool", "default": "false", - "note": "Whether show gene id or gene name" + "note": "Show exon id/exon name." }, { "key": "transcripts_to_show", "annotation": "str", "default": "", - "note": "Which transcript to show, transcript name or id in gtf file, eg: transcript1,transcript2" + "note": "To display specific transcripts, " + "their names or IDs must be included in the annotation file (GTF file). " + "If there are multiple transcripts to show, " + "please separate each transcript by a comma (iso1, iso2, iso3)." }, { "key": "intron_scale", "annotation": "float", "default": "0.5", - "note": "The scale of intron" + "note": "The degree of intron scaling. " + "The intron shrinkage scale determines the degree of intron length reduction. " + "A smaller value will result in a shorter intron length," + " effectively highlighting the exon structures." }, { "key": "exon_scale", "annotation": "float", "default": "1", - "note": "The scale of exon" + "note": "The degree of exon scaling. See intron_scale." }, ], "set_region": [ @@ -608,7 +614,6 @@ def __init__(self, key: str, annotation: str, default: str, note: Optional[str] class PlotParam: - __slots__ = ["path", "type", "param"] def __init__(self, path: str, type: Optional[str], param: List[Param]): @@ -639,11 +644,11 @@ def create(cls, data: dict): class PostForm: - __slots__ = ["region", "reference", "files", "draw"] + __slots__ = ["region", "annotation", "files", "draw"] - def __init__(self, region: PlotParam, reference: PlotParam, files: List[PlotParam], draw: PlotParam): + def __init__(self, region: PlotParam, annotation: PlotParam, files: List[PlotParam], draw: PlotParam): self.region = region - self.reference = reference + self.annotation = annotation self.files = files self.draw = draw @@ -746,14 +751,24 @@ def params(): target = request.args.get("target") res = [] + category = None for p in __PARAMS__[target]: if not isinstance(p, dict): for cp in __COMMON_PARAMS__: if cp["key"] in p: - res.append(cp) + + if cp["key"] == "category" and target in __SUPPORT_FORMAT__: + category = cp + category["annotation"] = f"choice[{','.join(__SUPPORT_FORMAT__[target])}]" + category["default"] = __SUPPORT_FORMAT__[target][0] + else: + res.append(cp) else: res.append(p) + if category: + res.append(category) + return jsonify(res) @@ -767,7 +782,7 @@ def plot(pid: str): if os.path.exists(log): os.remove(log) - p = Plot(log) + p = Plot(logfile=log, backend="agg") if param: with open(os.path.join(__PLOT__, pid), "wb+") as w: @@ -794,8 +809,8 @@ def plot_form_to_dict(param_: PlotParam): # set region p.set_region(**plot_form_to_dict(param.region)) - # set reference - p.set_reference(param.reference.path, **plot_form_to_dict(param.reference)) + # set annotation + p.set_annotation(param.annotation.path, **plot_form_to_dict(param.annotation)) # set files for param_ in param.files: @@ -834,7 +849,7 @@ def plot_form_to_dict(param_: PlotParam): return send_file(o, mimetype=f"image/pdf", as_attachment=True, download_name=f"{p.region}.pdf") except Exception as err: - logger.error(err) + logger.exception(err) return jsonify(str(err)), 501 return jsonify("") @@ -868,19 +883,22 @@ def logs(): with open(logfile) as r: for line in r: try: - time, level, info = line.strip().split("|") - infos = info.split("-") - source = infos[0] - - if not debug and "DEBUG" in level: - continue - - log_info.append(vars(Logs( - time=time.strip(), - level=level.strip(), - source=source.strip(), - message=info.replace(source, "").strip() - ))) + if "|" not in line and len(log_info) > 0: + log_info[-1]['message'] = f"{log_info[-1]['message']}\n{line}" + else: + time, level, info = line.strip().split("|") + infos = info.split("-") + source = infos[0] + + if not debug and "DEBUG" in level: + continue + + log_info.append(vars(Logs( + time=time.strip(), + level=level.strip(), + source=source.strip(), + message=info.replace(source, "").strip() + ))) except Exception as err: log_info.append(vars(Logs( time="", @@ -910,7 +928,7 @@ def main(host: str, port: int, plots: str, data: str): __DIR__ = data os.makedirs(__PLOT__, exist_ok=True) - app.run(host=host, port=port) + app.run(host=host, port=port, debug=False) if __name__ == '__main__': diff --git a/trackplot/base/Transcript.py b/trackplot/base/Transcript.py index d089745..2b751c9 100644 --- a/trackplot/base/Transcript.py +++ b/trackplot/base/Transcript.py @@ -116,6 +116,30 @@ def __hash__(self): exons = sorted([str(x.__hash__()) for x in self.exons]) return hash((self.chromosome, self.start, self.end, self.strand, " ".join(exons))) + def __lt__(self, other): + if self.chromosome != other.chromosome: + return self.chromosome < other.chromosome + + if self.start != other.start: + return self.start < other.start + + if self.end != other.end: + return self.end < other.end + + return len(self.exons) < len(other.exons) + + def __gt__(self, other): + if self.chromosome != other.chromosome: + return self.chromosome > other.chromosome + + if self.start != other.start: + return self.start > other.start + + if self.end != other.end: + return self.end > other.end + + return len(self.exons) > len(other.exons) + def ids(self) -> List[str]: return [self.transcript, self.transcript_id, self.gene, self.gene_id] diff --git a/trackplot/cli.py b/trackplot/cli.py index ab5c4d7..7808354 100644 --- a/trackplot/cli.py +++ b/trackplot/cli.py @@ -18,11 +18,7 @@ from trackplot.base.GenomicLoci import GenomicLoci from trackplot.conf.config import CLUSTERING_METHOD, COLORS, COLORMAP, DISTANCE_METRIC, IMAGE_TYPE, NORMALIZATION from trackplot.file.ATAC import ATAC -from trackplot.plot import Plot - -__version__ = "0.2.7" -__author__ = "ygidtu & Ran Zhou" -__email__ = "ygidtu@gmail.com" +from trackplot.plot import Plot, __version__ def decode_region(region: str): diff --git a/trackplot/conf/config.py b/trackplot/conf/config.py index 7ef5b15..bde83f7 100644 --- a/trackplot/conf/config.py +++ b/trackplot/conf/config.py @@ -13,7 +13,7 @@ 'pink', 'spring', 'summer', 'autumn', 'winter', 'cool', 'Wistia', 'hot', 'afmhot', 'gist_heat', 'copper', 'PiYG', 'PRGn', 'BrBG', 'PuOr', 'RdGy', 'RdBu', 'RdYlBu', - 'RdYlGn', 'Spectral', 'coolwarm', 'bwr', 'seismic' + 'RdYlGn', 'Spectral', 'coolwarm', 'bwr', 'seismic', "RdYlBu_r" ] DISTANCE_METRIC = [ diff --git a/trackplot/file/Annotation.py b/trackplot/file/Annotation.py index ddfc605..db984d5 100644 --- a/trackplot/file/Annotation.py +++ b/trackplot/file/Annotation.py @@ -64,8 +64,8 @@ def __init__(self, path: str, category: str = "gtf", logger.info(f"Using proxy: {proxy}") def __add__(self, other): - assert isinstance(other, Reference), "only Reference and Reference could be added" - new_ref = Reference(self.path, category=self.category) + assert isinstance(other, Annotation), "only Annotation and Annotation could be added" + new_ref = Annotation(self.path, category=self.category) new_ref.data += self.data new_ref.data += other.data new_ref.data = sorted(new_ref.data) @@ -75,7 +75,7 @@ def __add__(self, other): if self.domain: new_ref.__add_domain__() if self.add_local_domain: - new_ref.__load_local_domain__() + new_ref.__load_local_domain__(self.region) return new_ref def len(self, scale: Union[int, float] = .25) -> int: @@ -112,7 +112,7 @@ def create(cls, path: str, :param domain_exclude: the domain will be included in reference plot :param domain_include: the domain will be excluded in reference plot :param category: the type of reference file, include gtf and bam: customized reads as references - :return: Reference obj + :return: Annotation obj """ assert os.path.exists(path), f"{path} not exists" diff --git a/trackplot/plot.py b/trackplot/plot.py index cf62ebc..065a2ec 100644 --- a/trackplot/plot.py +++ b/trackplot/plot.py @@ -31,6 +31,11 @@ faulthandler.enable() +__version__ = "0.2.8" +__author__ = "ygidtu & Ran Zhou" +__email__ = "ygidtu@gmail.com" + + def __load__(args): p, args, kwargs = args p.load(*args, **kwargs) @@ -144,6 +149,18 @@ def load(self, region: GenomicLoci, n_jobs: int = 0, *args, **kwargs): return self +def add_object_error(func): + def inner(*args, **kwargs): + # calling the actual function now + # inside the wrapper function. + try: + func(*args, **kwargs) + except Exception as err: + logger.error(f"trackplot will ignore this object, {err}") + + return inner + + class Plot(object): u""" this class is the main framework of sashimi @@ -178,7 +195,9 @@ def __init__(self, self.link = [] if logfile: - logger.add(logfile, level="TRACE") + logger.add(logfile, level="DEBUG") + + logger.info(f"Create trackplot version: {__version__}") # print warning info about backend if backend: @@ -221,6 +240,7 @@ def exons(self) -> Optional[List[List[int]]]: if self.annotation: return self.annotation.exons + @add_object_error def set_region(self, chromosome: str = "", start: int = 0, end: int = 0, strand: str = "+", @@ -236,6 +256,7 @@ def set_region(self, chromosome: str = "", logger.info(f"set region to {self.region}") return self + @add_object_error def add_sites(self, sites): u""" highlight specific sites @@ -243,8 +264,9 @@ def add_sites(self, sites): :return: """ assert self.region is not None, f"please set plot region first." - logger.info(f"add sites: {sites}") + if sites is not None: + logger.info(f"add sites: {sites}") if isinstance(sites, str): sites = [int(x) for x in sites.split(",")] @@ -262,6 +284,7 @@ def add_sites(self, sites): self.sites[sites] = "red" return self + @add_object_error def add_focus(self, focus: Optional[str] = None, start: int = 0, end: int = 0): u""" set focus region @@ -288,6 +311,7 @@ def add_focus(self, focus: Optional[str] = None, start: int = 0, end: int = 0): self.focus[start] = max(end, self.focus.get(start, -1)) return self + @add_object_error def add_stroke( self, stroke: Optional[str] = None, @@ -315,6 +339,7 @@ def add_stroke( self.stroke.append(Stroke(start - self.start, end - self.end, color, label)) return self + @add_object_error def add_links( self, link: Optional[str] = None, @@ -343,6 +368,7 @@ def add_links( self.link.append([Stroke(start - self.start, end - self.end, color, label)]) return self + @add_object_error def set_sequence(self, fasta: str): u""" set sequence info for @@ -353,6 +379,7 @@ def set_sequence(self, fasta: str): self.sequence = Fasta.create(fasta) return self + @add_object_error def set_annotation(self, gtf: str, add_domain: bool = False, local_domain: Optional[str] = False, @@ -422,6 +449,7 @@ def set_annotation(self, gtf: str, return self + @add_object_error def add_interval(self, interval: str, interval_label: str): assert self.annotation is not None, "please set_annotation first." logger.info(f"add interval: {interval} - {interval_label}") @@ -486,7 +514,7 @@ def __init_input_file__(self, path: str, del_ratio_ignore=del_ratio_ignore, exon_focus=exon_focus ) - elif category == "hic": + elif category == "hic" or category == "h5": obj = HiCTrack.create( path=path, label=label, @@ -504,15 +532,17 @@ def __init_input_file__(self, path: str, obj = Depth.create(path, label=label, title=title) else: raise ValueError( - f"the category should be one of [bam, bigwig, bw, depth, bedgraph, bg], instead of {category}") + f"the category should be one of [bam, bigwig, bw, depth, bedgraph, bg, h5], instead of {category}") obj.log_trans = log_trans return obj, category + @add_object_error def add_customized_junctions(self, path: str): if path and os.path.exists(path) and os.path.isfile(path): self.junctions = load_custom_junction(path) + @add_object_error def add_density(self, path: str, category: str = "bam", @@ -590,7 +620,8 @@ def add_density(self, if show_site_plot and category == "bam": type_ = "site-plot" elif show_site_plot: - logger.debug("show_site_plot only works with bam files") + if category != "bam": + raise ValueError("show_site_plot only works with bam files") info = PlotInfo(obj=obj, type_=type_, category=category) @@ -624,6 +655,7 @@ def add_density(self, } return self + @add_object_error def add_heatmap(self, path: str, group: str = "", @@ -723,6 +755,7 @@ def add_heatmap(self, return self + @add_object_error def add_line(self, path: str, group: str = "", @@ -815,6 +848,7 @@ def add_line(self, return self + @add_object_error def add_hic( self, path: str, @@ -850,9 +884,9 @@ def add_hic( "show_y_label": show_y_label, "theme": theme } - return self + @add_object_error def add_igv( self, path: str, @@ -922,6 +956,7 @@ def add_igv( return self + @add_object_error def add_motif(self, path: str, category: str = "motif", @@ -944,6 +979,7 @@ def add_motif(self, self.params[info] = {"width": width, "theme": theme} return self + @add_object_error def add_manual(self, data: np.array, image_type: str = "line", @@ -1254,11 +1290,12 @@ def plot(self, if self.annotation is not None: logger.info(f"plotting annotation at idx: {curr_idx} with height_ratio: {height_ratio[curr_idx]}") ax_var = plt.subplot(gs[curr_idx:curr_idx + self.annotation.len(scale=annotation_scale), 0]) - plot_annotation(ax=ax_var, obj=self.annotation, - graph_coords=self.graph_coords, - plot_domain=self.annotation.add_domain, - distance_between_label_axis=distance_between_label_axis, - **self.params["annotation"]) + plot_annotation( + ax=ax_var, obj=self.annotation, + graph_coords=self.graph_coords, + plot_domain=self.annotation.add_domain, + distance_between_label_axis=distance_between_label_axis, + **self.params["annotation"]) # adjust indicator lines and focus background set_indicator_lines(ax=ax_var, sites=self.sites, graph_coords=self.graph_coords) diff --git a/trackplot/plot_func.py b/trackplot/plot_func.py index 71ea1f3..1a69314 100644 --- a/trackplot/plot_func.py +++ b/trackplot/plot_func.py @@ -417,7 +417,7 @@ def plot_annotation( Maybe I'm too stupid for this, using 30% of total length of x axis as the gap between text with axis """ - for transcript in obj.data: + for transcript in data: # ignore the unwanted transcript if transcripts and not (set(transcripts) & set(transcript.ids())): continue @@ -494,7 +494,6 @@ def plot_annotation( ax.plot(x, y, lw=.5, color=color, rasterized=raster) y_loc += 1 # if transcript.transcript else .5 - if plot_domain and obj.domain and transcript.transcript_id in obj.domain.pep: current_domains = obj.domain.pep[transcript.transcript_id] @@ -521,8 +520,7 @@ def plot_annotation( y_loc - exon_width / 4, y_loc - exon_width / 4, y_loc + exon_width / 4, y_loc + exon_width / 4 ] - ax.fill(x, y, color, lw=.5, - zorder=20, rasterized=raster) + ax.fill(x, y, color, lw=.5, zorder=20, rasterized=raster) # @2022.05.13 intron_relative_s = region.relative( @@ -541,8 +539,8 @@ def plot_annotation( intron_sites = [graph_coords[intron_relative_s], graph_coords[intron_relative_e]] if len(sub_exon) != 1: - ax.plot(intron_sites, [y_loc, y_loc], - color=color, lw=0.2, rasterized=raster) + ax.plot(intron_sites, [y_loc, y_loc], color=color, lw=0.2, rasterized=raster) + if show_id: ax.text(x=-1, y=y_loc - 0.125, s=f"{sub_current_domain.gene}|{transcript.transcript_id}", fontsize=font_size / 2, ha="right") @@ -550,10 +548,8 @@ def plot_annotation( ax.text(x=-1, y=y_loc - 0.125, s=f"{sub_current_domain.gene}|{transcript.transcript}", fontsize=font_size / 2, ha="right") - y_loc += 0.25 + y_loc += .5 - # offset for next term. - y_loc += 0.75 if obj.local_domain: for base_name, current_domain in obj.local_domain.items(): for sub_current_domain in current_domain: diff --git a/web/components.d.ts b/web/components.d.ts index 6f59066..d12612f 100644 --- a/web/components.d.ts +++ b/web/components.d.ts @@ -8,6 +8,7 @@ export {} declare module '@vue/runtime-core' { export interface GlobalComponents { Add: typeof import('./src/components/Add.vue')['default'] + Annotation: typeof import('./src/components/Annotation.vue')['default'] ElButton: typeof import('element-plus/es')['ElButton'] ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup'] ElCol: typeof import('element-plus/es')['ElCol'] @@ -37,9 +38,10 @@ declare module '@vue/runtime-core' { ElTabPane: typeof import('element-plus/es')['ElTabPane'] ElTabs: typeof import('element-plus/es')['ElTabs'] ElText: typeof import('element-plus/es')['ElText'] + ElTimeline: typeof import('element-plus/es')['ElTimeline'] + ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem'] Log: typeof import('./src/components/Log.vue')['default'] Param: typeof import('./src/components/Param.vue')['default'] - Reference: typeof import('./src/components/Reference.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] } diff --git a/web/package.json b/web/package.json index 2592c21..8d17a68 100644 --- a/web/package.json +++ b/web/package.json @@ -1,7 +1,7 @@ { "name": "trackplot", "private": true, - "version": "0.2.7", + "version": "0.2.8", "type": "module", "scripts": { "dev": "vite", diff --git a/web/src/components/Reference.vue b/web/src/components/Annotation.vue similarity index 88% rename from web/src/components/Reference.vue rename to web/src/components/Annotation.vue index 71e8020..7ad8fd4 100644 --- a/web/src/components/Reference.vue +++ b/web/src/components/Annotation.vue @@ -2,7 +2,7 @@
- +
@@ -18,7 +18,7 @@ import urls from '../url'; import {errorPrint, Notification} from "../error"; export default { - name: "reference", + name: "annotation", data() { return { @@ -31,7 +31,7 @@ export default { params: {"target": data.path, valid: true}, }).then((response: any) => { if (response.data) { - data.type = "reference" + data.type = "annotation" this.submit(data) } else { let msg: Notification = { diff --git a/web/src/components/Param.vue b/web/src/components/Param.vue index b9cc754..886f511 100644 --- a/web/src/components/Param.vue +++ b/web/src/components/Param.vue @@ -18,6 +18,10 @@ import {Message, Document, Folder, View, Download} from '@element-plus/icons-vue {{i}} + + {{i}} + + @@ -107,12 +111,12 @@ interface Data { const decodeChoice = (choices: String) => { let res = Array(); - let choiceMatch = choices.match(/choice\[(.*)\]/); + let choiceMatch = choices.match(/(choice|select)\[(.*)\]/); if (choiceMatch === null) { return res } - let choice = choiceMatch[1].toString().replaceAll("'", "").replaceAll('"', '').split(','); + let choice = choiceMatch[2].toString().replaceAll("'", "").replaceAll('"', '').split(','); for (let c of choice) { res.push(c.trim()) @@ -158,6 +162,9 @@ export default defineComponent({ } else if (row.annotation.startsWith("choice")) { row.choice = decodeChoice(row.annotation) row.annotation = "choice" + } else if (row.annotation.startsWith("select")) { + row.choice = decodeChoice(row.annotation) + row.annotation = "select" } this.param.push(row) diff --git a/web/src/pages/Plot.vue b/web/src/pages/Plot.vue index 63edeba..874e2d5 100644 --- a/web/src/pages/Plot.vue +++ b/web/src/pages/Plot.vue @@ -1,8 +1,8 @@ @@ -17,8 +17,7 @@ import LogComp from "../components/Log.vue" + :active="active"> @@ -53,7 +52,7 @@ import LogComp from "../components/Log.vue" - + @@ -87,7 +86,7 @@ import LogComp from "../components/Log.vue" - + @@ -98,10 +97,13 @@ import LogComp from "../components/Log.vue" trigger="hover" > - {{ p.default }} + {{ p.default }} + @@ -111,22 +113,27 @@ import LogComp from "../components/Log.vue" - - - - - - - {{ p.default }} - - - - + +
+ + + + + + {{ p.default }} + + + + + + + +
@@ -209,7 +216,7 @@ interface FilePath { interface Progress { region: string | null, - reference: FilePath | null, + annotation: FilePath | null, files: FilePath[] | null, draw: FilePath | null } @@ -229,7 +236,7 @@ export default defineComponent({ data() { let progress: Progress = { region: null, - reference: null, + annotation: null, files: [], draw: null } @@ -280,8 +287,8 @@ export default defineComponent({ }, handleClose() { this.img = null }, makeProgress(file: FilePath) { - if (file.type === "reference") { - this.progress.reference = file + if (file.type === "annotation") { + this.progress.annotation = file } else if (file.type !== "plot" && file.type !== "save") { this.progress.files?.push(file) } else if (file.type === "plot" || file.type == "save") { @@ -335,6 +342,15 @@ export default defineComponent({ loading.close() }) }, + removeThis(idx: Number) { + let kept = []; + for (let i = 0; i < this.progress.files.length; i++) { + if (i !== idx) { + kept.push(this.progress.files[i]) + } + } + this.progress.files = kept + } }, computed: { showImage() { return this.img !== null }, @@ -350,7 +366,10 @@ export default defineComponent({ this.$cookie.setCookie("plot", (Math.random() + 1).toString(36).substring(7)) // this.$cookie.setCookie("plot", "test") this.pid = this.$cookie.getCookie("plot") - } + }, + beforeUnmount() { + this.axios.get(`${urls.del}?pid=${this.$cookie.getCookie("plot")}`) + }, }) diff --git a/webAppImageBuilder.yml b/webAppImageBuilder.yml index ae3f1ba..0da9ee0 100644 --- a/webAppImageBuilder.yml +++ b/webAppImageBuilder.yml @@ -10,7 +10,7 @@ AppDir: app_info: id: org.appimage-crafters.trackplotweb name: trackplotweb - version: 0.2.7 + version: 0.2.8 # Set the python executable as entry point exec: "bin/python3" # Set the application main script path as argument. Use '$@' to forward CLI parameters