mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-09 23:10:24 +01:00
Add plots props and plots modify params (#3911)
* Add plots props and `plots modify` params * Update dvc/command/plots.py Co-authored-by: Ruslan Kuprieiev <[email protected]> * Update dvc/repo/plots/show.py Co-authored-by: Ruslan Kuprieiev <[email protected]> * Update dvc/repo/plots/show.py Co-authored-by: Ruslan Kuprieiev <[email protected]> * Update dvc/repo/plots/show.py Co-authored-by: Ruslan Kuprieiev <[email protected]> * Update dvc/repo/plots/show.py Co-authored-by: Ruslan Kuprieiev <[email protected]> Co-authored-by: Ruslan Kuprieiev <[email protected]>
This commit is contained in:
co-authored by
Ruslan Kuprieiev
parent
ec312d8601
commit
516d82e411
+65
-80
@@ -4,6 +4,7 @@ import os
|
||||
|
||||
from dvc.command.base import CmdBase, append_doc_link, fix_subparsers
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.schema import PLOT_PROPS
|
||||
from dvc.utils import format_link
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -32,6 +33,11 @@ class CmdPlots(CmdBase):
|
||||
def _func(self, *args, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
def _props(self):
|
||||
# Pass only props specified by user, to not shadow ones from plot def
|
||||
props = {p: getattr(self.args, p) for p in PLOT_PROPS}
|
||||
return {k: v for k, v in props.items() if v is not None}
|
||||
|
||||
def run(self):
|
||||
if self.args.show_vega:
|
||||
if not self.args.targets:
|
||||
@@ -44,16 +50,7 @@ class CmdPlots(CmdBase):
|
||||
return 1
|
||||
|
||||
try:
|
||||
plots = self._func(
|
||||
targets=self.args.targets,
|
||||
template=self.args.template,
|
||||
x_field=self.args.x,
|
||||
y_field=self.args.y,
|
||||
csv_header=not self.args.no_csv_header,
|
||||
title=self.args.title,
|
||||
x_title=self.args.xlab,
|
||||
y_title=self.args.ylab,
|
||||
)
|
||||
plots = self._func(targets=self.args.targets, props=self._props())
|
||||
|
||||
if self.args.show_vega:
|
||||
target = self.args.targets[0]
|
||||
@@ -91,6 +88,14 @@ class CmdPlotsDiff(CmdPlots):
|
||||
return self.repo.plots.diff(*args, revs=self.args.revisions, **kwargs)
|
||||
|
||||
|
||||
class CmdPlotsModify(CmdPlots):
|
||||
def run(self):
|
||||
self.repo.plots.modify(
|
||||
self.args.target, props=self._props(), unset=self.args.unset,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
PLOTS_HELP = (
|
||||
"Generating plots for metrics stored in structured files "
|
||||
@@ -119,51 +124,13 @@ def add_parser(subparsers, parent_parser):
|
||||
help=SHOW_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
plots_show_parser.add_argument(
|
||||
"-t",
|
||||
"--template",
|
||||
nargs="?",
|
||||
default=None,
|
||||
help=(
|
||||
"Special JSON or HTML schema file to inject with the data. "
|
||||
"See {}".format(
|
||||
format_link("https://man.dvc.org/plots#plot-templates")
|
||||
)
|
||||
),
|
||||
)
|
||||
plots_show_parser.add_argument(
|
||||
"-o", "--out", default=None, help="Destination path to save plots to.",
|
||||
)
|
||||
plots_show_parser.add_argument(
|
||||
"-x", default=None, help="Field name for x axis."
|
||||
)
|
||||
plots_show_parser.add_argument(
|
||||
"-y", default=None, help="Field name for y axis."
|
||||
)
|
||||
plots_show_parser.add_argument(
|
||||
"--no-csv-header",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Required when CSV or TSV datafile does not have a header.",
|
||||
)
|
||||
plots_show_parser.add_argument(
|
||||
"--show-vega",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show output in VEGA format.",
|
||||
)
|
||||
plots_show_parser.add_argument("--title", default=None, help="Plot title.")
|
||||
plots_show_parser.add_argument(
|
||||
"--xlab", default=None, help="X axis title."
|
||||
)
|
||||
plots_show_parser.add_argument(
|
||||
"--ylab", default=None, help="Y axis title."
|
||||
)
|
||||
plots_show_parser.add_argument(
|
||||
"targets",
|
||||
nargs="*",
|
||||
help="Metrics files to visualize. Shows all plots by default.",
|
||||
help="Plots files to visualize. Shows all plots by default.",
|
||||
)
|
||||
_add_props_arguments(plots_show_parser)
|
||||
_add_output_arguments(plots_show_parser)
|
||||
plots_show_parser.set_defaults(func=CmdPlotsShow)
|
||||
|
||||
PLOTS_DIFF_HELP = (
|
||||
@@ -178,6 +145,37 @@ def add_parser(subparsers, parent_parser):
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
plots_diff_parser.add_argument(
|
||||
"--targets",
|
||||
nargs="*",
|
||||
help="Plots file to visualize. Shows all plots by default.",
|
||||
)
|
||||
plots_diff_parser.add_argument(
|
||||
"revisions", nargs="*", default=None, help="Git commits to plot from",
|
||||
)
|
||||
_add_props_arguments(plots_diff_parser)
|
||||
_add_output_arguments(plots_diff_parser)
|
||||
plots_diff_parser.set_defaults(func=CmdPlotsDiff)
|
||||
|
||||
PLOTS_MODIFY_HELP = "Modify plot props associated with a target file."
|
||||
plots_modify_parser = plots_subparsers.add_parser(
|
||||
"modify",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(PLOTS_MODIFY_HELP, "plots/modify"),
|
||||
help=PLOTS_MODIFY_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
plots_modify_parser.add_argument(
|
||||
"target", help="Plot file to set props to.",
|
||||
)
|
||||
_add_props_arguments(plots_modify_parser)
|
||||
plots_modify_parser.add_argument(
|
||||
"--unset", nargs="*", help="Props to unset.",
|
||||
)
|
||||
plots_modify_parser.set_defaults(func=CmdPlotsModify)
|
||||
|
||||
|
||||
def _add_props_arguments(parser):
|
||||
parser.add_argument(
|
||||
"-t",
|
||||
"--template",
|
||||
nargs="?",
|
||||
@@ -189,40 +187,27 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
),
|
||||
)
|
||||
plots_diff_parser.add_argument(
|
||||
"--targets",
|
||||
nargs="*",
|
||||
help="Metrics file to visualize. Shows all plots by default.",
|
||||
)
|
||||
plots_diff_parser.add_argument(
|
||||
"-o", "--out", default=None, help="Destination path to save plots to.",
|
||||
)
|
||||
plots_diff_parser.add_argument(
|
||||
"-x", default=None, help="Field name for x axis."
|
||||
)
|
||||
plots_diff_parser.add_argument(
|
||||
"-y", default=None, help="Field name for y axis."
|
||||
)
|
||||
plots_diff_parser.add_argument(
|
||||
parser.add_argument("-x", default=None, help="Field name for x axis.")
|
||||
parser.add_argument("-y", default=None, help="Field name for y axis.")
|
||||
parser.add_argument(
|
||||
"--no-csv-header",
|
||||
action="store_true",
|
||||
default=False,
|
||||
action="store_false",
|
||||
dest="csv_header",
|
||||
default=None, # Use default None to distinguish when it's not used
|
||||
help="Provided CSV ot TSV datafile does not have a header.",
|
||||
)
|
||||
plots_diff_parser.add_argument(
|
||||
parser.add_argument("--title", default=None, help="Plot title.")
|
||||
parser.add_argument("--xlab", default=None, help="X axis title.")
|
||||
parser.add_argument("--ylab", default=None, help="Y axis title.")
|
||||
|
||||
|
||||
def _add_output_arguments(parser):
|
||||
parser.add_argument(
|
||||
"-o", "--out", default=None, help="Destination path to save plots to.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--show-vega",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show output in VEGA format.",
|
||||
)
|
||||
plots_diff_parser.add_argument("--title", default=None, help="Plot title.")
|
||||
plots_diff_parser.add_argument(
|
||||
"--xlab", default=None, help="X axis title."
|
||||
)
|
||||
plots_diff_parser.add_argument(
|
||||
"--ylab", default=None, help="Y axis title."
|
||||
)
|
||||
plots_diff_parser.add_argument(
|
||||
"revisions", nargs="*", default=None, help="Git commits to plot from",
|
||||
)
|
||||
plots_diff_parser.set_defaults(func=CmdPlotsDiff)
|
||||
|
||||
@@ -56,6 +56,12 @@ class BaseOutput:
|
||||
PARAM_METRIC_XPATH = "xpath"
|
||||
PARAM_PLOT = "plot"
|
||||
PARAM_PLOT_TEMPLATE = "template"
|
||||
PARAM_PLOT_X = "x"
|
||||
PARAM_PLOT_Y = "y"
|
||||
PARAM_PLOT_XLAB = "xlab"
|
||||
PARAM_PLOT_YLAB = "ylab"
|
||||
PARAM_PLOT_TITLE = "title"
|
||||
PARAM_PLOT_CSV_HEADER = "csv_header"
|
||||
PARAM_PERSIST = "persist"
|
||||
|
||||
METRIC_SCHEMA = Any(
|
||||
|
||||
@@ -441,7 +441,7 @@ class Repo:
|
||||
stages = stages or self.stages
|
||||
outs = Trie() # Use trie to efficiently find overlapping outs and deps
|
||||
|
||||
for stage in filter(bool, stages):
|
||||
for stage in filter(bool, stages): # bug? not using it later
|
||||
for out in stage.outs:
|
||||
out_key = out.path_info.parts
|
||||
|
||||
|
||||
@@ -11,3 +11,21 @@ class Plots:
|
||||
from .diff import diff
|
||||
|
||||
return diff(self.repo, *args, **kwargs)
|
||||
|
||||
def modify(self, path, props=None, unset=None):
|
||||
from dvc.dvcfile import Dvcfile
|
||||
|
||||
(out,) = self.repo.find_outs_by_path(path)
|
||||
|
||||
# This out will become a plot unless it is one already
|
||||
if not out.plot:
|
||||
out.plot = {}
|
||||
|
||||
for field in unset or ():
|
||||
out.plot.pop(field, None)
|
||||
out.plot.update(props or {})
|
||||
|
||||
out.verify_metric()
|
||||
|
||||
dvcfile = Dvcfile(self.repo, out.stage.path)
|
||||
dvcfile.dump(out.stage, update_pipeline=True)
|
||||
|
||||
+85
-89
@@ -1,13 +1,14 @@
|
||||
import copy
|
||||
import logging
|
||||
import os
|
||||
from collections import defaultdict
|
||||
|
||||
from funcy import first, last
|
||||
from funcy import first, last, project
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.repo import locked
|
||||
from dvc.schema import PLOT_PROPS
|
||||
|
||||
from .data import PlotData
|
||||
from .data import NoMetricInHistoryError, PlotData
|
||||
from .template import NoDataForTemplateError, Template
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -43,29 +44,24 @@ def _evaluate_templatepath(repo, template=None):
|
||||
|
||||
@locked
|
||||
def fill_template(
|
||||
repo,
|
||||
datafile,
|
||||
template_path,
|
||||
revisions,
|
||||
fields=None,
|
||||
path=None,
|
||||
csv_header=True,
|
||||
x_field=None,
|
||||
y_field=None,
|
||||
**kwargs,
|
||||
repo, datafile, template_path, revisions, props,
|
||||
):
|
||||
if x_field and fields:
|
||||
fields.add(x_field)
|
||||
# Copy things to not modify passed values
|
||||
props = props.copy()
|
||||
fields = copy.copy(props.get("fields"))
|
||||
|
||||
if y_field and fields:
|
||||
fields.add(y_field)
|
||||
if props.get("x") and fields:
|
||||
fields.add(props.get("x"))
|
||||
|
||||
if props.get("y") and fields:
|
||||
fields.add(props.get("y"))
|
||||
|
||||
template_datafiles, x_anchor, y_anchor = _parse_template(
|
||||
template_path, datafile
|
||||
)
|
||||
append_index = x_anchor and not x_field
|
||||
append_index = x_anchor and not props.get("x")
|
||||
if append_index:
|
||||
x_field = PlotData.INDEX_FIELD
|
||||
props["x"] = PlotData.INDEX_FIELD
|
||||
|
||||
template_data = {}
|
||||
for template_datafile in template_datafiles:
|
||||
@@ -76,13 +72,13 @@ def fill_template(
|
||||
for pd in plot_datas:
|
||||
rev_data_points = pd.to_datapoints(
|
||||
fields=fields,
|
||||
path=path,
|
||||
csv_header=csv_header,
|
||||
path=props.get("path"),
|
||||
csv_header=props.get("csv_header", True),
|
||||
append_index=append_index,
|
||||
)
|
||||
|
||||
if y_anchor and not y_field:
|
||||
y_field = _infer_y_field(rev_data_points, x_field)
|
||||
if y_anchor and not props.get("y"):
|
||||
props["y"] = _infer_y_field(rev_data_points, props.get("x"))
|
||||
tmp_data.extend(rev_data_points)
|
||||
|
||||
template_data[template_datafile] = tmp_data
|
||||
@@ -91,12 +87,7 @@ def fill_template(
|
||||
raise NoDataForTemplateError(template_path)
|
||||
|
||||
content = Template.fill(
|
||||
template_path,
|
||||
template_data,
|
||||
priority_datafile=datafile,
|
||||
x_field=x_field,
|
||||
y_field=y_field,
|
||||
**kwargs,
|
||||
template_path, template_data, priority_datafile=datafile, props=props,
|
||||
)
|
||||
|
||||
path = datafile or ",".join(template_datafiles)
|
||||
@@ -113,17 +104,17 @@ def _infer_y_field(rev_data_points, x_field):
|
||||
return y_field
|
||||
|
||||
|
||||
def _show(repo, datafile=None, template=None, revs=None, **kwargs):
|
||||
def _show(repo, datafile=None, revs=None, props=None):
|
||||
if revs is None:
|
||||
revs = ["working tree"]
|
||||
|
||||
if not datafile and not template:
|
||||
if not datafile and not props.get("template"):
|
||||
raise NoDataOrTemplateProvided()
|
||||
|
||||
template_path = _evaluate_templatepath(repo, template)
|
||||
template_path = _evaluate_templatepath(repo, props.get("template"))
|
||||
|
||||
plot_datafile, plot_content = fill_template(
|
||||
repo, datafile, template_path, revs, **kwargs
|
||||
repo, datafile, template_path, revs, props
|
||||
)
|
||||
|
||||
return plot_datafile, plot_content
|
||||
@@ -148,67 +139,72 @@ def _parse_template(template_path, priority_datafile):
|
||||
)
|
||||
|
||||
|
||||
def _collect_plots(repo):
|
||||
plots = defaultdict(set)
|
||||
def _collect_plots(repo, targets=None):
|
||||
from dvc.exceptions import OutputNotFoundError
|
||||
from contextlib import suppress
|
||||
|
||||
for stage in repo.stages:
|
||||
for out in stage.outs:
|
||||
if not out.plot:
|
||||
continue
|
||||
|
||||
if isinstance(out.plot, dict):
|
||||
template = out.plot[out.PARAM_PLOT_TEMPLATE]
|
||||
else:
|
||||
template = None
|
||||
|
||||
plots[str(out)].add(template)
|
||||
|
||||
return plots
|
||||
|
||||
|
||||
def show(repo, targets=None, template=None, revs=None, **kwargs) -> dict:
|
||||
if isinstance(targets, str):
|
||||
targets = [targets]
|
||||
def _targets_to_outs(targets):
|
||||
for t in targets:
|
||||
with suppress(OutputNotFoundError):
|
||||
(out,) = repo.find_outs_by_path(t)
|
||||
yield out
|
||||
|
||||
if targets:
|
||||
for target in targets:
|
||||
return {
|
||||
target: _show(
|
||||
repo,
|
||||
datafile=target,
|
||||
template=template,
|
||||
revs=revs,
|
||||
**kwargs,
|
||||
)[1]
|
||||
}
|
||||
|
||||
if not revs:
|
||||
plots = _collect_plots(repo)
|
||||
outs = _targets_to_outs(targets)
|
||||
else:
|
||||
plots = defaultdict(set)
|
||||
for rev in repo.brancher(revs=revs):
|
||||
for plot, templates in _collect_plots(repo).items():
|
||||
plots[plot].update(templates)
|
||||
outs = (out for stage in repo.stages for out in stage.outs if out.plot)
|
||||
|
||||
return {str(out): _plot_props(out) for out in outs}
|
||||
|
||||
|
||||
def _plot_props(out):
|
||||
if not out.plot:
|
||||
raise DvcException(
|
||||
f"'{out}' is not a plot. Use `dvc plots modify` to change that."
|
||||
)
|
||||
if isinstance(out.plot, list):
|
||||
raise DvcException("Multiple plots per data file not supported yet.")
|
||||
if isinstance(out.plot, bool):
|
||||
return {}
|
||||
|
||||
return project(out.plot, PLOT_PROPS)
|
||||
|
||||
|
||||
def show(repo, targets=None, revs=None, props=None) -> dict:
|
||||
if isinstance(targets, str):
|
||||
targets = [targets]
|
||||
if props is None:
|
||||
props = {}
|
||||
|
||||
# Collect plot data files with associated props
|
||||
plots = {}
|
||||
for rev in repo.brancher(revs=revs):
|
||||
if revs is not None and rev not in revs:
|
||||
continue
|
||||
|
||||
for datafile, file_props in _collect_plots(repo, targets).items():
|
||||
# props from command line overwrite plot props from out definition
|
||||
full_props = {**file_props, **props}
|
||||
|
||||
if datafile in plots:
|
||||
saved_rev, saved_props = plots[datafile]
|
||||
if saved_props != props:
|
||||
logger.warning(
|
||||
f"Inconsistent plot props for '{datafile}' in "
|
||||
f"'{saved_rev}' and '{rev}'. "
|
||||
f"Going to use ones from '{saved_rev}'"
|
||||
)
|
||||
else:
|
||||
plots[datafile] = rev, full_props
|
||||
|
||||
if not plots:
|
||||
datafile, plot = _show(
|
||||
repo, datafile=None, template=template, revs=revs, **kwargs
|
||||
)
|
||||
if targets:
|
||||
raise NoMetricInHistoryError(", ".join(targets))
|
||||
|
||||
datafile, plot = _show(repo, datafile=None, revs=revs, props=props)
|
||||
return {datafile: plot}
|
||||
|
||||
ret = {}
|
||||
for plot, templates in plots.items():
|
||||
tmplt = template
|
||||
if len(templates) == 1:
|
||||
tmplt = list(templates)[0]
|
||||
elif not template:
|
||||
raise TooManyTemplatesError(
|
||||
f"'{plot}' uses multiple templates '{templates}'. "
|
||||
"Use `-t|--template` to specify the template to use. "
|
||||
)
|
||||
|
||||
ret[plot] = _show(
|
||||
repo, datafile=plot, template=tmplt, revs=revs, **kwargs
|
||||
)[1]
|
||||
|
||||
return ret
|
||||
return {
|
||||
datafile: _show(repo, datafile=datafile, revs=revs, props=props)[1]
|
||||
for datafile, (_, props) in plots.items()
|
||||
}
|
||||
|
||||
+25
-41
@@ -86,29 +86,24 @@ class Template:
|
||||
|
||||
@staticmethod
|
||||
def fill(
|
||||
template_path,
|
||||
data,
|
||||
priority_datafile=None,
|
||||
x_field=None,
|
||||
y_field=None,
|
||||
title=None,
|
||||
x_title=None,
|
||||
y_title=None,
|
||||
template_path, data, priority_datafile=None, props=None,
|
||||
):
|
||||
props = props or {}
|
||||
|
||||
with open(template_path) as fobj:
|
||||
result_content = fobj.read()
|
||||
|
||||
if x_field:
|
||||
Template._check_field_exists(data, x_field)
|
||||
if y_field:
|
||||
Template._check_field_exists(data, y_field)
|
||||
if props.get("x"):
|
||||
Template._check_field_exists(data, props.get("x"))
|
||||
if props.get("y"):
|
||||
Template._check_field_exists(data, props.get("y"))
|
||||
|
||||
result_content = Template._replace_data_anchors(
|
||||
result_content, data, priority_datafile
|
||||
)
|
||||
|
||||
result_content = Template._replace_metadata_anchors(
|
||||
result_content, title, x_field, x_title, y_field, y_title
|
||||
result_content, props
|
||||
)
|
||||
|
||||
return result_content
|
||||
@@ -122,34 +117,23 @@ class Template:
|
||||
raise NoFieldInDataError(field)
|
||||
|
||||
@staticmethod
|
||||
def _replace_metadata_anchors(
|
||||
result_content, title, x_field, x_title, y_field, y_title
|
||||
):
|
||||
if Template.TITLE_ANCHOR in result_content:
|
||||
if title:
|
||||
result_content = result_content.replace(
|
||||
Template.TITLE_ANCHOR, title
|
||||
)
|
||||
else:
|
||||
result_content = result_content.replace(
|
||||
Template.TITLE_ANCHOR, ""
|
||||
)
|
||||
if Template.X_ANCHOR in result_content and x_field:
|
||||
result_content = result_content.replace(Template.X_ANCHOR, x_field)
|
||||
if Template.Y_ANCHOR in result_content and y_field:
|
||||
result_content = result_content.replace(Template.Y_ANCHOR, y_field)
|
||||
if Template.X_TITLE_ANCHOR in result_content:
|
||||
if not x_title and x_field:
|
||||
x_title = x_field
|
||||
result_content = result_content.replace(
|
||||
Template.X_TITLE_ANCHOR, x_title
|
||||
)
|
||||
if Template.Y_TITLE_ANCHOR in result_content:
|
||||
if not y_title and y_field:
|
||||
y_title = y_field
|
||||
result_content = result_content.replace(
|
||||
Template.Y_TITLE_ANCHOR, y_title
|
||||
)
|
||||
def _replace_metadata_anchors(result_content, props):
|
||||
props.setdefault("title", "")
|
||||
props.setdefault("xlab", props.get("x"))
|
||||
props.setdefault("ylab", props.get("y"))
|
||||
|
||||
replace_pairs = [
|
||||
(Template.TITLE_ANCHOR, "title"),
|
||||
(Template.X_ANCHOR, "x"),
|
||||
(Template.Y_ANCHOR, "y"),
|
||||
(Template.X_TITLE_ANCHOR, "xlab"),
|
||||
(Template.Y_TITLE_ANCHOR, "ylab"),
|
||||
]
|
||||
for anchor, key in replace_pairs:
|
||||
value = props.get(key)
|
||||
if anchor in result_content and value is not None:
|
||||
result_content = result_content.replace(anchor, value)
|
||||
|
||||
return result_content
|
||||
|
||||
@staticmethod
|
||||
|
||||
+14
-2
@@ -29,8 +29,20 @@ OUT_PSTAGE_DETAILED_SCHEMA = {
|
||||
str: {BaseOutput.PARAM_CACHE: bool, BaseOutput.PARAM_PERSIST: bool}
|
||||
}
|
||||
|
||||
PLOT_PSTAGE_SCHEMA = OUT_PSTAGE_DETAILED_SCHEMA.copy()
|
||||
PLOT_PSTAGE_SCHEMA[str][BaseOutput.PARAM_PLOT_TEMPLATE] = str
|
||||
PLOT_PROPS = {
|
||||
BaseOutput.PARAM_PLOT_TEMPLATE: str,
|
||||
BaseOutput.PARAM_PLOT_X: str,
|
||||
BaseOutput.PARAM_PLOT_Y: str,
|
||||
BaseOutput.PARAM_PLOT_XLAB: str,
|
||||
BaseOutput.PARAM_PLOT_YLAB: str,
|
||||
BaseOutput.PARAM_PLOT_TITLE: str,
|
||||
BaseOutput.PARAM_PLOT_CSV_HEADER: bool,
|
||||
}
|
||||
PLOT_PROPS_SCHEMA = {
|
||||
**OUT_PSTAGE_DETAILED_SCHEMA[str],
|
||||
**PLOT_PROPS,
|
||||
}
|
||||
PLOT_PSTAGE_SCHEMA = {str: Any(PLOT_PROPS_SCHEMA, [PLOT_PROPS_SCHEMA])}
|
||||
|
||||
PARAM_PSTAGE_NON_DEFAULT_SCHEMA = {str: [str]}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ def test_diff_dirty(tmp_dir, scm, dvc, run_copy_metrics):
|
||||
"metric_t.json", "metric.json", plots_no_cache=["metric.json"]
|
||||
)
|
||||
|
||||
plot_string = dvc.plots.diff(fields={"y"})["metric.json"]
|
||||
plot_string = dvc.plots.diff(props={"fields": {"y"}})["metric.json"]
|
||||
|
||||
plot_content = json.loads(plot_string)
|
||||
assert plot_content["data"]["values"] == [
|
||||
|
||||
@@ -22,10 +22,6 @@ from dvc.repo.plots.template import (
|
||||
)
|
||||
|
||||
|
||||
def _remove_whitespace(value):
|
||||
return value.replace(" ", "").replace("\n", "")
|
||||
|
||||
|
||||
def _write_csv(metric, filename, header=True):
|
||||
with open(filename, "w", newline="") as csvobj:
|
||||
if header:
|
||||
@@ -53,12 +49,13 @@ def test_plot_csv_one_column(tmp_dir, scm, dvc, run_copy_metrics):
|
||||
"metric_t.csv", "metric.csv", plots_no_cache=["metric.csv"]
|
||||
)
|
||||
|
||||
plot_string = dvc.plots.show(
|
||||
csv_header=False,
|
||||
x_title="x_title",
|
||||
y_title="y_title",
|
||||
title="mytitle",
|
||||
)["metric.csv"]
|
||||
props = {
|
||||
"csv_header": False,
|
||||
"xlab": "x_title",
|
||||
"ylab": "y_title",
|
||||
"title": "mytitle",
|
||||
}
|
||||
plot_string = dvc.plots.show(props=props)["metric.csv"]
|
||||
|
||||
plot_content = json.loads(plot_string)
|
||||
assert plot_content["title"] == "mytitle"
|
||||
@@ -115,9 +112,8 @@ def test_plot_csv_choose_axes(tmp_dir, scm, dvc, run_copy_metrics):
|
||||
"metric_t.csv", "metric.csv", plots_no_cache=["metric.csv"]
|
||||
)
|
||||
|
||||
plot_string = dvc.plots.show(x_field="first_val", y_field="second_val")[
|
||||
"metric.csv"
|
||||
]
|
||||
props = {"x": "first_val", "y": "second_val"}
|
||||
plot_string = dvc.plots.show(props=props)["metric.csv"]
|
||||
|
||||
plot_content = json.loads(plot_string)
|
||||
assert plot_content["data"]["values"] == [
|
||||
@@ -206,9 +202,8 @@ def test_plot_confusion(tmp_dir, dvc, run_copy_metrics):
|
||||
commit="first run",
|
||||
)
|
||||
|
||||
plot_string = dvc.plots.show(
|
||||
template="confusion", x_field="predicted", y_field="actual",
|
||||
)["metric.json"]
|
||||
props = {"template": "confusion", "x": "predicted", "y": "actual"}
|
||||
plot_string = dvc.plots.show(props=props)["metric.json"]
|
||||
|
||||
plot_content = json.loads(plot_string)
|
||||
assert plot_content["data"]["values"] == [
|
||||
@@ -248,9 +243,9 @@ def test_plot_multiple_revs_default(tmp_dir, scm, dvc, run_copy_metrics):
|
||||
plots_no_cache=["metric.json"],
|
||||
commit="third",
|
||||
)
|
||||
plot_string = dvc.plots.show(fields={"y"}, revs=["HEAD", "v2", "v1"],)[
|
||||
"metric.json"
|
||||
]
|
||||
plot_string = dvc.plots.show(
|
||||
revs=["HEAD", "v2", "v1"], props={"fields": {"y"}}
|
||||
)["metric.json"]
|
||||
|
||||
plot_content = json.loads(plot_string)
|
||||
assert plot_content["data"]["values"] == [
|
||||
@@ -291,9 +286,10 @@ def test_plot_multiple_revs(tmp_dir, scm, dvc, run_copy_metrics):
|
||||
scm.add(["metric.json", stage.path])
|
||||
scm.commit("third")
|
||||
|
||||
plot_string = dvc.plots.show(
|
||||
template="template.json", revs=["HEAD", "v2", "v1"],
|
||||
)["metric.json"]
|
||||
props = {"template": "template.json"}
|
||||
plot_string = dvc.plots.show(revs=["HEAD", "v2", "v1"], props=props)[
|
||||
"metric.json"
|
||||
]
|
||||
|
||||
plot_content = json.loads(plot_string)
|
||||
assert plot_content["data"]["values"] == [
|
||||
@@ -379,9 +375,8 @@ def test_custom_template(tmp_dir, scm, dvc, custom_template, run_copy_metrics):
|
||||
tag="v1",
|
||||
)
|
||||
|
||||
plot_string = dvc.plots.show(
|
||||
template=os.fspath(custom_template), x_field="a", y_field="b"
|
||||
)["metric.json"]
|
||||
props = {"template": os.fspath(custom_template), "x": "a", "y": "b"}
|
||||
plot_string = dvc.plots.show(props=props)["metric.json"]
|
||||
|
||||
plot_content = json.loads(plot_string)
|
||||
assert plot_content["data"]["values"] == [
|
||||
@@ -413,9 +408,8 @@ def test_custom_template_with_specified_data(
|
||||
tag="v1",
|
||||
)
|
||||
|
||||
plot_string = dvc.plots.show(
|
||||
template=os.fspath(custom_template), x_field="a", y_field="b",
|
||||
)["metric.json"]
|
||||
props = {"template": os.fspath(custom_template), "x": "a", "y": "b"}
|
||||
plot_string = dvc.plots.show(props=props)["metric.json"]
|
||||
|
||||
plot_content = json.loads(plot_string)
|
||||
assert plot_content["data"]["values"] == [
|
||||
@@ -444,14 +438,15 @@ def test_plot_override_specified_data_source(
|
||||
run_copy_metrics(
|
||||
"metric1.json",
|
||||
"metric2.json",
|
||||
outs_no_cache=["metric2.json"],
|
||||
plots_no_cache=["metric2.json"],
|
||||
commit="init",
|
||||
tag="v1",
|
||||
)
|
||||
|
||||
plot_string = dvc.plots.show(
|
||||
targets=["metric2.json"], template="newtemplate.json", x_field="a"
|
||||
)["metric2.json"]
|
||||
props = {"template": "newtemplate.json", "x": "a"}
|
||||
plot_string = dvc.plots.show(targets=["metric2.json"], props=props)[
|
||||
"metric2.json"
|
||||
]
|
||||
|
||||
plot_content = json.loads(plot_string)
|
||||
assert plot_content["data"]["values"] == [
|
||||
@@ -467,18 +462,35 @@ def test_should_raise_on_no_template_and_datafile(tmp_dir, dvc):
|
||||
dvc.plots.show()
|
||||
|
||||
|
||||
def test_should_raise_on_no_template(tmp_dir, dvc):
|
||||
def test_should_raise_on_no_template(tmp_dir, dvc, run_copy_metrics):
|
||||
metric = [{"val": 2}, {"val": 3}]
|
||||
_write_json(tmp_dir, metric, "metric_t.json")
|
||||
run_copy_metrics(
|
||||
"metric_t.json",
|
||||
"metric.json",
|
||||
plots_no_cache=["metric.json"],
|
||||
commit="first run",
|
||||
)
|
||||
|
||||
with pytest.raises(TemplateNotFoundError):
|
||||
dvc.plots.show("metric.json", template="non_existing_template.json")
|
||||
props = {"template": "non_existing_template.json"}
|
||||
dvc.plots.show("metric.json", props=props)
|
||||
|
||||
|
||||
def test_plot_no_data(tmp_dir, dvc):
|
||||
with pytest.raises(NoDataForTemplateError):
|
||||
dvc.plots.show(template="default")
|
||||
dvc.plots.show(props={"template": "default"})
|
||||
|
||||
|
||||
def test_plot_wrong_metric_type(tmp_dir, scm, dvc):
|
||||
tmp_dir.scm_gen("metric.txt", "content", commit="initial")
|
||||
def test_plot_wrong_metric_type(tmp_dir, scm, dvc, run_copy_metrics):
|
||||
tmp_dir.gen("metric_t.txt", "some text")
|
||||
run_copy_metrics(
|
||||
"metric_t.txt",
|
||||
"metric.txt",
|
||||
plots_no_cache=["metric.txt"],
|
||||
commit="add text metric",
|
||||
)
|
||||
|
||||
with pytest.raises(PlotMetricTypeError):
|
||||
dvc.plots.show(targets=["metric.txt"])
|
||||
|
||||
@@ -496,12 +508,13 @@ def test_plot_choose_columns(
|
||||
tag="v1",
|
||||
)
|
||||
|
||||
plot_string = dvc.plots.show(
|
||||
template=os.fspath(custom_template),
|
||||
fields={"b", "c"},
|
||||
x_field="b",
|
||||
y_field="c",
|
||||
)["metric.json"]
|
||||
props = {
|
||||
"template": os.fspath(custom_template),
|
||||
"fields": {"b", "c"},
|
||||
"x": "b",
|
||||
"y": "c",
|
||||
}
|
||||
plot_string = dvc.plots.show(props=props)["metric.json"]
|
||||
|
||||
plot_content = json.loads(plot_string)
|
||||
assert plot_content["data"]["values"] == [
|
||||
@@ -523,7 +536,7 @@ def test_plot_default_choose_column(tmp_dir, scm, dvc, run_copy_metrics):
|
||||
tag="v1",
|
||||
)
|
||||
|
||||
plot_string = dvc.plots.show(fields={"b"})["metric.json"]
|
||||
plot_string = dvc.plots.show(props={"fields": {"b"}})["metric.json"]
|
||||
|
||||
plot_content = json.loads(plot_string)
|
||||
assert plot_content["data"]["values"] == [
|
||||
@@ -563,7 +576,7 @@ def test_raise_on_wrong_field(tmp_dir, scm, dvc, run_copy_metrics):
|
||||
)
|
||||
|
||||
with pytest.raises(NoFieldInDataError):
|
||||
dvc.plots.show("metric.json", x_field="no_val")
|
||||
dvc.plots.show("metric.json", props={"x": "no_val"})
|
||||
|
||||
with pytest.raises(NoFieldInDataError):
|
||||
dvc.plots.show("metric.json", y_field="no_val")
|
||||
dvc.plots.show("metric.json", props={"y": "no_val"})
|
||||
|
||||
@@ -41,14 +41,15 @@ def test_metrics_diff(dvc, mocker):
|
||||
m.assert_called_once_with(
|
||||
cmd.repo,
|
||||
targets=["datafile"],
|
||||
template="template",
|
||||
revs=["HEAD", "tag1", "tag2"],
|
||||
x_field="x_field",
|
||||
y_field="y_field",
|
||||
csv_header=True,
|
||||
title="my_title",
|
||||
x_title="x_title",
|
||||
y_title="y_title",
|
||||
props={
|
||||
"template": "template",
|
||||
"x": "x_field",
|
||||
"y": "y_field",
|
||||
"title": "my_title",
|
||||
"xlab": "x_title",
|
||||
"ylab": "y_title",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -79,13 +80,7 @@ def test_metrics_show(dvc, mocker):
|
||||
m.assert_called_once_with(
|
||||
cmd.repo,
|
||||
targets=["datafile"],
|
||||
template="template",
|
||||
x_field=None,
|
||||
y_field=None,
|
||||
csv_header=False,
|
||||
title=None,
|
||||
x_title=None,
|
||||
y_title=None,
|
||||
props={"template": "template", "csv_header": False},
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user