Repository: iterative/dvc

Issue:
snap: 'sudo' cannot be used with build provider 'lxd'
https://travis-ci.com/github/iterative/dvc/jobs/346490988#L495

Discussion:
casperdcl: Hmm Travis has a radically different [guide suggesting docker](https://snapcraft.io/tutorials/continuous-snap-delivery-from-travis-ci#5-building-and-pushing-the-snap-in-travis-ci), which concludes [on the next page](https://snapcraft.io/tutorials/continuous-snap-delivery-from-travis-ci#6-now-lets-do-some-crowd-testing) to [check out `cmake`](https://github.com/Crascit/cmake-snap/blob/master/.travis.yml), which has moved on to a different approach almost identical to what we use here. Looks like minor changes are required to fix the `sudo`. Will do.

Patch:
diff --git a/.travis.yml b/.travis.yml
index e0b14cc42..de7d7a517 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -98,8 +98,12 @@ jobs:
      - name: lxd
        channel: stable
     env:
-    - SNAPCRAFT_IMAGE_INFO: '{"build_url": "$TRAVIS_BUILD_URL"}'
+    - SNAPCRAFT_IMAGE_INFO: |
+        '{"build_url": "$TRAVIS_JOB_WEB_URL"}'
+    - SNAPCRAFT_BUILD_ENVIRONMENT: lxd
+    - SNAPCRAFT_BUILD_INFO: 1  # https://snapcraft.io/blog/introducing-developer-notifications-for-snap-security-updates
     install:
+    - sudo usermod --append --groups lxd $USER
     - sudo /snap/bin/lxd.migrate -yes
     - sudo /snap/bin/lxd waitready
     - sudo /snap/bin/lxd init --auto
diff --git a/dvc/repo/plots/__init__.py b/dvc/repo/plots/__init__.py
index 11681368a..a76a89701 100644
--- a/dvc/repo/plots/__init__.py
+++ b/dvc/repo/plots/__init__.py
@@ -1,11 +1,80 @@
+import logging
+
+from funcy import first, project
+
+from dvc.exceptions import DvcException, NoPlotsError, OutputNotFoundError
+from dvc.repo.tree import RepoTree
+from dvc.schema import PLOT_PROPS
+
+logger = logging.getLogger(__name__)
+
+
 class Plots:
     def __init__(self, repo):
         self.repo = repo
 
-    def show(self, *args, **kwargs):
-        from .show import show
+    def collect(self, targets=None, revs=None):
+        """Collects all props and data for plots.
+
+        Returns a structure like:
+            {rev: {plots.csv: {
+                props: {x: ..., "csv_header": ..., ...},
+                data: "...data as a string...",
+            }}}
+        Data parsing is postponed, since it's affected by props.
+        """
+        targets = [targets] if isinstance(targets, str) else targets or []
+        data = {}
+        for rev in self.repo.brancher(revs=revs):
+            # .brancher() adds unwanted workspace
+            if revs is not None and rev not in revs:
+                continue
+            rev = rev or "workspace"
+
+            tree = RepoTree(self.repo)
+            plots = _collect_plots(self.repo, targets, rev)
+            for datafile, props in plots.items():
+                data[rev] = {datafile: {"props": props}}
+
+                # Load data from git or dvc cache
+                try:
+                    with tree.open(datafile) as fd:
+                        data[rev][datafile]["data"] = fd.read()
+                except FileNotFoundError:
+                    # This might happen simply because cache is absent
+                    pass
+
+        return data
+
+    def render(self, data, revs=None, props=None, templates=None):
+        """Renders plots"""
+        props = props or {}
+        templates = templates or self.repo.plot_templates
 
-        return show(self.repo, *args, **kwargs)
+        # Merge data by plot file and apply overriding props
+        plots = _prepare_plots(data, revs, props)
+
+        return {
+            datafile: _render(datafile, desc["data"], desc["props"], templates)
+            for datafile, desc in plots.items()
+        }
+
+    def show(self, targets=None, revs=None, props=None):
+        from .data import NoMetricInHistoryError
+
+        data = self.collect(targets, revs)
+
+        # If any mentioned plot doesn't have any data then that's an error
+        targets = [targets] if isinstance(targets, str) else targets or []
+        for target in targets:
+            if not any("data" in d[target] for d in data.values()):
+                raise NoMetricInHistoryError(target)
+
+        # No data at all is a special error with a special message
+        if not data:
+            raise NoPlotsError()
+
+        return self.render(data, revs, props)
 
     def diff(self, *args, **kwargs):
         from .diff import diff
@@ -33,3 +102,113 @@ class Plots:
 
         dvcfile = Dvcfile(self.repo, out.stage.path)
         dvcfile.dump(out.stage, update_pipeline=True)
+
+
+def _collect_plots(repo, targets=None, rev=None):
+    def _targets_to_outs(targets):
+        for t in targets:
+            try:
+                (out,) = repo.find_outs_by_path(t)
+                yield out
+            except OutputNotFoundError:
+                logger.warning(
+                    "File '{}' was not found at: '{}'. It will not be "
+                    "plotted.".format(t, rev)
+                )
+
+    if targets:
+        outs = _targets_to_outs(targets)
+    else:
+        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.")
+    if isinstance(out.plot, bool):
+        return {}
+
+    return project(out.plot, PLOT_PROPS)
+
+
+def _prepare_plots(data, revs, props):
+    """Groups data by plot file.
+
+    Also resolves props conflicts between revs and applies global props.
+    """
+    # we go in order revs are supplied on props conflict first ones win.
+    revs = iter(data) if revs is None else revs
+
+    plots, props_revs = {}, {}
+    for rev in revs:
+        # Asked for revision without data
+        if rev not in data:
+            continue
+
+        for datafile, desc in data[rev].items():
+            # props from command line overwrite plot props from out definition
+            full_props = {**desc["props"], **props}
+
+            if datafile in plots:
+                saved = plots[datafile]
+                if saved["props"] != full_props:
+                    logger.warning(
+                        f"Inconsistent plot props for '{datafile}' in "
+                        f"'{props_revs[datafile]}' and '{rev}'. "
+                        f"Going to use ones from '{props_revs[datafile]}'"
+                    )
+
+                saved["data"][rev] = desc["data"]
+            else:
+                plots[datafile] = {
+                    "props": full_props,
+                    "data": {rev: desc["data"]},
+                }
+                # Save rev we got props from
+                props_revs[datafile] = rev
+
+    return plots
+
+
+def _render(datafile, datas, props, templates):
+    from .data import plot_data, PlotData
+
+    # Copy it to not modify a passed value
+    props = props.copy()
+
+    # Add x and y to fields if set
+    fields = props.get("fields")
+    if fields is not None:
+        fields = {*fields, props.get("x"), props.get("y")} - {None}
+
+    template = templates.load(props.get("template") or "default")
+
+    # If x is not set add index field
+    if not props.get("x") and template.has_anchor("x"):
+        props["append_index"] = True
+        props["x"] = PlotData.INDEX_FIELD
+
+    # Parse all data, preprocess it and collect as a list of dicts
+    data = []
+    for rev, datablob in datas.items():
+        rev_data = plot_data(datafile, rev, datablob).to_datapoints(
+            fields=fields,
+            path=props.get("path"),
+            csv_header=props.get("csv_header", True),
+            append_index=props.get("append_index", False),
+        )
+        data.extend(rev_data)
+
+    # If y is not set then use last field not used yet
+    if not props.get("y") and template.has_anchor("y"):
+        fields = list(first(data))
+        skip = (PlotData.REVISION_FIELD, props.get("x"))
+        props["y"] = first(f for f in reversed(fields) if f not in skip)
+
+    return template.render(data, props=props)
diff --git a/dvc/repo/plots/data.py b/dvc/repo/plots/data.py
index 901548928..0cce97364 100644
--- a/dvc/repo/plots/data.py
+++ b/dvc/repo/plots/data.py
@@ -1,7 +1,6 @@
 import csv
 import io
 import json
-import logging
 import os
 from collections import OrderedDict
 from copy import copy
@@ -10,9 +9,7 @@ import yaml
 from funcy import first
 from yaml import SafeLoader
 
-from dvc.exceptions import DvcException, PathMissingError
-
-logger = logging.getLogger(__name__)
+from dvc.exceptions import DvcException
 
 
 class PlotMetricTypeError(DvcException):
@@ -31,21 +28,6 @@ class PlotDataStructureError(DvcException):
         )
 
 
-class JsonParsingError(DvcException):
-    def __init__(self, file):
-        super().__init__(
-            "Failed to infer data structure from '{}'. Did you forget "
-            "to specify JSONpath?".format(file)
-        )
-
-
-class NoMetricOnRevisionError(DvcException):
-    def __init__(self, path, revision):
-        self.path = path
-        self.revision = revision
-        super().__init__(f"Could not find '{path}' on revision '{revision}'")
-
-
 class NoMetricInHistoryError(DvcException):
     def __init__(self, path):
         super().__init__(f"Could not find '{path}'.")
@@ -77,7 +59,7 @@ def _filter_fields(data_points, filename, revision, fields=None, **kwargs):
         if keys & fields != fields:
             raise DvcException(
                 "Could not find fields: '{}' for '{}' at '{}'.".format(
-                    ", " "".join(fields), filename, revision
+                    ", ".join(fields), filename, revision
                 )
             )
 
@@ -240,47 +222,3 @@ class YAMLPlotData(PlotData):
     def _processors(self):
         parent_processors = super()._processors()
         return [_find_data] + parent_processors
-
-
-def _load_from_revision(repo, datafile, revision):
-    from dvc.repo.tree import RepoTree
-
-    tree = RepoTree(repo)
-
-    try:
-        with tree.open(datafile) as fobj:
-            datafile_content = fobj.read()
-
-    except (FileNotFoundError, PathMissingError):
-        raise NoMetricOnRevisionError(datafile, revision)
-
-    return plot_data(datafile, revision, datafile_content)
-
-
-def _load_from_revisions(repo, datafile, revisions):
-    data = []
-    exceptions = []
-
-    for rev in repo.brancher(revs=revisions):
-        if rev == "workspace" and rev not in revisions:
-            continue
-
-        try:
-            data.append(_load_from_revision(repo, datafile, rev))
-        except NoMetricOnRevisionError as e:
-            exceptions.append(e)
-        except PlotMetricTypeError:
-            raise
-        except (yaml.error.YAMLError, json.decoder.JSONDecodeError, csv.Error):
-            logger.error(f"Failed to parse '{datafile}' at '{rev}'.")
-            raise
-
-    if not data and exceptions:
-        raise NoMetricInHistoryError(datafile)
-    else:
-        for e in exceptions:
-            logger.warning(
-                "File '{}' was not found at: '{}'. It will not be "
-                "plotted.".format(e.path, e.revision)
-            )
-    return data
diff --git a/dvc/repo/plots/show.py b/dvc/repo/plots/show.py
deleted file mode 100644
index 49c9f4f43..000000000
--- a/dvc/repo/plots/show.py
+++ /dev/null
@@ -1,217 +0,0 @@
-import copy
-import logging
-import os
-
-from funcy import first, last, project
-
-from dvc.exceptions import DvcException, NoPlotsError
-from dvc.repo import locked
-from dvc.schema import PLOT_PROPS
-
-from .data import NoMetricInHistoryError, PlotData
-from .template import NoDataForTemplateError, Template
-
-logger = logging.getLogger(__name__)
-
-
-class TooManyDataSourcesError(DvcException):
-    def __init__(self, datafile, template_datafiles):
-        super().__init__(
-            "Unable to infer which of possible data sources: '{}' "
-            "should be replaced with '{}'.".format(
-                ", ".join(template_datafiles), datafile
-            )
-        )
-
-
-class NoDataOrTemplateProvided(DvcException):
-    def __init__(self):
-        super().__init__("Datafile or template is not specified.")
-
-
-class TooManyTemplatesError(DvcException):
-    pass
-
-
-def _evaluate_templatepath(repo, template=None):
-    if not template:
-        return repo.plot_templates.default_template
-
-    if os.path.exists(template):
-        return template
-    return repo.plot_templates.get_template(template)
-
-
-@locked
-def fill_template(
-    repo, datafile, template_path, revisions, props,
-):
-    # Copy things to not modify passed values
-    props = props.copy()
-    fields = copy.copy(props.get("fields"))
-
-    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 props.get("x")
-    if append_index:
-        props["x"] = PlotData.INDEX_FIELD
-
-    template_data = {}
-    for template_datafile in template_datafiles:
-        from .data import _load_from_revisions
-
-        plot_datas = _load_from_revisions(repo, template_datafile, revisions)
-        tmp_data = []
-        for pd in plot_datas:
-            rev_data_points = pd.to_datapoints(
-                fields=fields,
-                path=props.get("path"),
-                csv_header=props.get("csv_header", True),
-                append_index=append_index,
-            )
-
-            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
-
-    if len(template_data) == 0:
-        raise NoDataForTemplateError(template_path)
-
-    content = Template.fill(
-        template_path, template_data, priority_datafile=datafile, props=props,
-    )
-
-    path = datafile or ",".join(template_datafiles)
-
-    return path, content
-
-
-def _infer_y_field(rev_data_points, x_field):
-    all_fields = list(first(rev_data_points).keys())
-    all_fields.remove(PlotData.REVISION_FIELD)
-    if x_field and x_field in all_fields:
-        all_fields.remove(x_field)
-    y_field = last(all_fields)
-    return y_field
-
-
-def _show(repo, datafile=None, revs=None, props=None):
-    if revs is None:
-        revs = ["workspace"]
-
-    if props is None:
-        props = {}
-
-    if not datafile and not props.get("template"):
-        raise NoDataOrTemplateProvided()
-
-    template_path = _evaluate_templatepath(repo, props.get("template"))
-
-    plot_datafile, plot_content = fill_template(
-        repo, datafile, template_path, revs, props
-    )
-
-    return plot_datafile, plot_content
-
-
-def _parse_template(template_path, priority_datafile):
-    with open(template_path) as fobj:
-        tempalte_content = fobj.read()
-
-    template_datafiles = Template.parse_data_anchors(tempalte_content)
-    if priority_datafile:
-        if len(template_datafiles) > 1:
-            raise TooManyDataSourcesError(
-                priority_datafile, template_datafiles
-            )
-        template_datafiles = {priority_datafile}
-
-    return (
-        template_datafiles,
-        Template.X_ANCHOR in tempalte_content,
-        Template.Y_ANCHOR in tempalte_content,
-    )
-
-
-def _collect_plots(repo, targets=None):
-    from dvc.exceptions import OutputNotFoundError
-    from contextlib import suppress
-
-    def _targets_to_outs(targets):
-        for t in targets:
-            with suppress(OutputNotFoundError):
-                (out,) = repo.find_outs_by_path(t)
-                yield out
-
-    if targets:
-        outs = _targets_to_outs(targets)
-    else:
-        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:
-        if targets:
-            raise NoMetricInHistoryError(", ".join(targets))
-
-        try:
-            datafile, plot = _show(repo, datafile=None, revs=revs, props=props)
-        except NoDataOrTemplateProvided:
-            raise NoPlotsError()
-
-        return {datafile: plot}
-
-    return {
-        datafile: _show(repo, datafile=datafile, revs=revs, props=props)[1]
-        for datafile, (_, props) in plots.items()
-    }
diff --git a/dvc/repo/plots/template.py b/dvc/repo/plots/template.py
index db2555da3..01f9a85c6 100644
--- a/dvc/repo/plots/template.py
+++ b/dvc/repo/plots/template.py
@@ -1,28 +1,17 @@
 import json
-import logging
 import os
-import re
 
 from funcy import cached_property
 
 from dvc.exceptions import DvcException
 from dvc.utils.fs import makedirs
 
-logger = logging.getLogger(__name__)
-
 
 class TemplateNotFoundError(DvcException):
     def __init__(self, path):
         super().__init__(f"Template '{path}' not found.")
 
 
-class NoDataForTemplateError(DvcException):
-    def __init__(self, template_path):
-        super().__init__(
-            "No data provided for '{}'.".format(os.path.relpath(template_path))
-        )
-
-
 class NoFieldInDataError(DvcException):
     def __init__(self, field_name):
         super().__init__(
@@ -34,6 +23,7 @@ class Template:
     INDENT = 4
     SEPARATORS = (",", ": ")
     EXTENSION = ".json"
+    ANCHOR = '"<DVC_METRIC_{}>"'
     METRIC_DATA_ANCHOR = "<DVC_METRIC_DATA>"
     X_ANCHOR = "<DVC_METRIC_X>"
     Y_ANCHOR = "<DVC_METRIC_Y>"
@@ -41,125 +31,60 @@ class Template:
     X_LABEL_ANCHOR = "<DVC_METRIC_X_LABEL>"
     Y_LABEL_ANCHOR = "<DVC_METRIC_Y_LABEL>"
 
-    def __init__(self, templates_dir):
-        self.plot_templates_dir = templates_dir
-
-    def dump(self):
-        makedirs(self.plot_templates_dir, exist_ok=True)
-
-        with open(
-            os.path.join(
-                self.plot_templates_dir, self.TEMPLATE_NAME + self.EXTENSION
-            ),
-            "w",
-        ) as fobj:
-            json.dump(
-                self.DEFAULT_CONTENT,
-                fobj,
-                indent=self.INDENT,
-                separators=self.SEPARATORS,
-            )
-            fobj.write("\n")
-
-    @staticmethod
-    def get_data_anchor(template_content):
-        regex = re.compile('"<DVC_METRIC_DATA[^>"]*>"')
-        return regex.findall(template_content)
+    def __init__(self, content=None, name=None):
+        self.content = self.DEFAULT_CONTENT if content is None else content
+        self.name = name or self.DEFAULT_NAME
+        self.filename = self.name + self.EXTENSION
 
-    @staticmethod
-    def parse_data_anchors(template_content):
-        data_files = {
-            Template.get_datafile(m)
-            for m in Template.get_data_anchor(template_content)
-        }
-        return {df for df in data_files if df}
-
-    @staticmethod
-    def get_datafile(anchor_string):
-        return (
-            anchor_string.replace("<", "")
-            .replace(">", "")
-            .replace('"', "")
-            .replace("DVC_METRIC_DATA", "")
-            .replace(",", "")
-        )
-
-    @staticmethod
-    def fill(
-        template_path, data, priority_datafile=None, props=None,
-    ):
+    def render(self, data, props=None):
         props = props or {}
 
-        with open(template_path) as fobj:
-            result_content = fobj.read()
-
         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
-        )
+        content = self._fill_anchor(self.content, "data", data)
+        content = self._fill_metadata(content, props)
 
-        result_content = Template._replace_metadata_anchors(
-            result_content, props
-        )
+        return content
 
-        return result_content
+    def has_anchor(self, name):
+        return self._anchor(name) in self.content
 
-    @staticmethod
-    def _check_field_exists(data, field):
-        for file, data_points in data.items():
-            if not any(
-                field in data_point.keys() for data_point in data_points
-            ):
-                raise NoFieldInDataError(field)
+    @classmethod
+    def _fill_anchor(cls, content, name, value):
+        value_str = json.dumps(
+            value, indent=cls.INDENT, separators=cls.SEPARATORS, sort_keys=True
+        )
+        return content.replace(cls._anchor(name), value_str)
 
-    @staticmethod
-    def _replace_metadata_anchors(result_content, props):
+    @classmethod
+    def _anchor(cls, name):
+        return cls.ANCHOR.format(name.upper())
+
+    @classmethod
+    def _fill_metadata(cls, content, props):
         props.setdefault("title", "")
         props.setdefault("x_label", props.get("x"))
         props.setdefault("y_label", props.get("y"))
 
-        replace_pairs = [
-            (Template.TITLE_ANCHOR, "title"),
-            (Template.X_ANCHOR, "x"),
-            (Template.Y_ANCHOR, "y"),
-            (Template.X_LABEL_ANCHOR, "x_label"),
-            (Template.Y_LABEL_ANCHOR, "y_label"),
-        ]
-        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)
+        names = ["title", "x", "y", "x_label", "y_label"]
+        for name in names:
+            value = props.get(name)
+            if value is not None:
+                content = cls._fill_anchor(content, name, value)
 
-        return result_content
+        return content
 
     @staticmethod
-    def _replace_data_anchors(result_content, data, priority_datafile):
-        for anchor in Template.get_data_anchor(result_content):
-            file = Template.get_datafile(anchor)
-
-            if not file or priority_datafile:
-                key = priority_datafile
-            else:
-                key = file
-
-            result_content = result_content.replace(
-                anchor,
-                json.dumps(
-                    data[key],
-                    indent=Template.INDENT,
-                    separators=Template.SEPARATORS,
-                    sort_keys=True,
-                ),
-            )
-        return result_content
+    def _check_field_exists(data, field):
+        if not any(field in row for row in data):
+            raise NoFieldInDataError(field)
 
 
 class DefaultLinearTemplate(Template):
-    TEMPLATE_NAME = "default"
+    DEFAULT_NAME = "default"
 
     DEFAULT_CONTENT = {
         "$schema": "https://vega.github.io/schema/vega-lite/v4.json",
@@ -184,7 +109,7 @@ class DefaultLinearTemplate(Template):
 
 
 class DefaultConfusionTemplate(Template):
-    TEMPLATE_NAME = "confusion"
+    DEFAULT_NAME = "confusion"
     DEFAULT_CONTENT = {
         "$schema": "https://vega.github.io/schema/vega-lite/v4.json",
         "data": {"values": Template.METRIC_DATA_ANCHOR},
@@ -210,7 +135,7 @@ class DefaultConfusionTemplate(Template):
 
 
 class DefaultScatterTemplate(Template):
-    TEMPLATE_NAME = "scatter"
+    DEFAULT_NAME = "scatter"
     DEFAULT_CONTENT = {
         "$schema": "https://vega.github.io/schema/vega-lite/v4.json",
         "data": {"values": Template.METRIC_DATA_ANCHOR},
@@ -253,6 +178,9 @@ class PlotTemplates:
         return default_plot_path
 
     def get_template(self, path):
+        if os.path.exists(path):
+            return path
+
         t_path = os.path.join(self.templates_dir, path)
         if os.path.exists(t_path):
             return t_path
@@ -279,4 +207,23 @@ class PlotTemplates:
         if not os.path.exists(self.templates_dir):
             makedirs(self.templates_dir, exist_ok=True)
             for t in self.TEMPLATES:
-                t(self.templates_dir).dump()
+                self.dump(t())
+
+    def dump(self, template):
+        path = os.path.join(self.templates_dir, template.filename)
+        with open(path, "w") as fd:
+            json.dump(
+                template.content,
+                fd,
+                indent=template.INDENT,
+                separators=template.SEPARATORS,
+            )
+            fd.write("\n")
+
+    def load(self, name):
+        path = self.get_template(name)
+
+        with open(path) as fd:
+            content = fd.read()
+
+        return Template(content, name=name)
diff --git a/scripts/build_snap.sh b/scripts/build_snap.sh
index 93e7d51cc..3d1ea8547 100755
--- a/scripts/build_snap.sh
+++ b/scripts/build_snap.sh
@@ -7,7 +7,7 @@ if [ ! -d "dvc" ]; then
   exit 1
 fi
 
-sudo snapcraft --use-lxd
+sg lxd -c snapcraft
 
 pip uninstall -y dvc
 if which dvc; then
diff --git a/scripts/ci/before_install.sh b/scripts/ci/before_install.sh
index 1a02b3387..b16a58105 100644
--- a/scripts/ci/before_install.sh
+++ b/scripts/ci/before_install.sh
@@ -53,10 +53,10 @@ fi
 
 if [[ -n "$TRAVIS_TAG" ]]; then
   if [[ $(echo "$TRAVIS_TAG" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$') ]]; then
-    echo "export SNAP_CHANNEL=stable" >>env.sh
+    echo "export SNAP_CHANNEL=stable,v1/stable" >>env.sh
   else
-    echo "export SNAP_CHANNEL=beta" >>env.sh
+    echo "export SNAP_CHANNEL=beta,v1/beta" >>env.sh
   fi
 else
-  echo "export SNAP_CHANNEL=edge" >>env.sh
+  echo "export SNAP_CHANNEL=edge,v1/edge" >>env.sh
 fi


Test patch:
diff --git a/tests/func/plots/test_diff.py b/tests/func/plots/test_diff.py
index beb6a93f6..a59802398 100644
--- a/tests/func/plots/test_diff.py
+++ b/tests/func/plots/test_diff.py
@@ -32,10 +32,10 @@ def test_diff_dirty(tmp_dir, scm, dvc, run_copy_metrics):
 
     plot_content = json.loads(plot_string)
     assert plot_content["data"]["values"] == [
-        {"y": 5, PlotData.INDEX_FIELD: 0, "rev": "workspace"},
-        {"y": 6, PlotData.INDEX_FIELD: 1, "rev": "workspace"},
         {"y": 3, PlotData.INDEX_FIELD: 0, "rev": "HEAD"},
         {"y": 5, PlotData.INDEX_FIELD: 1, "rev": "HEAD"},
+        {"y": 5, PlotData.INDEX_FIELD: 0, "rev": "workspace"},
+        {"y": 6, PlotData.INDEX_FIELD: 1, "rev": "workspace"},
     ]
     assert plot_content["encoding"]["x"]["field"] == PlotData.INDEX_FIELD
     assert plot_content["encoding"]["y"]["field"] == "y"
diff --git a/tests/func/plots/test_plots.py b/tests/func/plots/test_plots.py
index cbd0afc07..365ae9823 100644
--- a/tests/func/plots/test_plots.py
+++ b/tests/func/plots/test_plots.py
@@ -16,11 +16,7 @@ from dvc.repo.plots.data import (
     PlotMetricTypeError,
     YAMLPlotData,
 )
-from dvc.repo.plots.template import (
-    NoDataForTemplateError,
-    NoFieldInDataError,
-    TemplateNotFoundError,
-)
+from dvc.repo.plots.template import NoFieldInDataError, TemplateNotFoundError
 
 
 def _write_csv(metric, filename, header=True):
@@ -262,7 +258,11 @@ def test_plot_multiple_revs_default(tmp_dir, scm, dvc, run_copy_metrics):
 
 
 def test_plot_multiple_revs(tmp_dir, scm, dvc, run_copy_metrics):
-    shutil.copy(tmp_dir / ".dvc" / "plots" / "default.json", "template.json")
+    templates_dir = dvc.plot_templates.templates_dir
+    shutil.copy(
+        os.path.join(templates_dir, "default.json"),
+        os.path.join(templates_dir, "template.json"),
+    )
 
     metric_1 = [{"y": 2}, {"y": 3}]
     _write_json(tmp_dir, metric_1, "metric_t.json")
@@ -323,13 +323,13 @@ def test_plot_even_if_metric_missing(
 
     caplog.clear()
     with caplog.at_level(logging.WARNING, "dvc"):
-        plot_string = dvc.plots.show(revs=["v1", "v2"])["metric.json"]
+        plots = dvc.plots.show(revs=["v1", "v2"], targets=["metric.json"])
         assert (
             "File 'metric.json' was not found at: 'v1'. "
             "It will not be plotted." in caplog.text
         )
 
-    plot_content = json.loads(plot_string)
+    plot_content = json.loads(plots["metric.json"])
     assert plot_content["data"]["values"] == [
         {"y": 2, PlotData.INDEX_FIELD: 0, "rev": "v2"},
         {"y": 3, PlotData.INDEX_FIELD: 1, "rev": "v2"},
@@ -392,72 +392,6 @@ def _replace(path, src, dst):
     path.write_text(path.read_text().replace(src, dst))
 
 
-def test_custom_template_with_specified_data(
-    tmp_dir, scm, dvc, custom_template, run_copy_metrics
-):
-    _replace(
-        custom_template, "DVC_METRIC_DATA", "DVC_METRIC_DATA,metric.json",
-    )
-
-    metric = [{"a": 1, "b": 2}, {"a": 2, "b": 3}]
-    _write_json(tmp_dir, metric, "metric_t.json")
-    run_copy_metrics(
-        "metric_t.json",
-        "metric.json",
-        outs_no_cache=["metric.json"],
-        commit="init",
-        tag="v1",
-    )
-
-    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"] == [
-        {"a": 1, "b": 2, "rev": "workspace"},
-        {"a": 2, "b": 3, "rev": "workspace"},
-    ]
-    assert plot_content["encoding"]["x"]["field"] == "a"
-    assert plot_content["encoding"]["y"]["field"] == "b"
-
-
-def test_plot_override_specified_data_source(
-    tmp_dir, scm, dvc, run_copy_metrics
-):
-    shutil.copy(
-        tmp_dir / ".dvc" / "plots" / "default.json",
-        tmp_dir / "newtemplate.json",
-    )
-    _replace(
-        tmp_dir / "newtemplate.json",
-        "DVC_METRIC_DATA",
-        "DVC_METRIC_DATA,metric.json",
-    )
-
-    metric = [{"a": 1, "b": 2}, {"a": 2, "b": 3}]
-    _write_json(tmp_dir, metric, "metric1.json")
-    run_copy_metrics(
-        "metric1.json",
-        "metric2.json",
-        plots_no_cache=["metric2.json"],
-        commit="init",
-        tag="v1",
-    )
-
-    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"] == [
-        {"a": 1, "b": 2, "rev": "workspace"},
-        {"a": 2, "b": 3, "rev": "workspace"},
-    ]
-    assert plot_content["encoding"]["x"]["field"] == "a"
-    assert plot_content["encoding"]["y"]["field"] == "b"
-
-
 def test_no_plots(tmp_dir, dvc):
     from dvc.exceptions import NoPlotsError
 
@@ -480,11 +414,6 @@ def test_should_raise_on_no_template(tmp_dir, dvc, run_copy_metrics):
         dvc.plots.show("metric.json", props=props)
 
 
-def test_plot_no_data(tmp_dir, dvc):
-    with pytest.raises(NoDataForTemplateError):
-        dvc.plots.show(props={"template": "default"})
-
-
 def test_plot_wrong_metric_type(tmp_dir, scm, dvc, run_copy_metrics):
     tmp_dir.gen("metric_t.txt", "some text")
     run_copy_metrics(
diff --git a/tests/unit/command/test_plots.py b/tests/unit/command/test_plots.py
index 4c1776c1b..8dba519af 100644
--- a/tests/unit/command/test_plots.py
+++ b/tests/unit/command/test_plots.py
@@ -72,13 +72,13 @@ def test_metrics_show(dvc, mocker):
     cmd = cli_args.func(cli_args)
 
     m = mocker.patch(
-        "dvc.repo.plots.show.show", return_value={"datafile": "filledtemplate"}
+        "dvc.repo.plots.Plots.show",
+        return_value={"datafile": "filledtemplate"},
     )
 
     assert cmd.run() == 0
 
     m.assert_called_once_with(
-        cmd.repo,
         targets=["datafile"],
         props={"template": "template", "csv_header": False},
     )