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)

Make plots reading and rendering API friendly
Context: this will help using it in viewer.

This things need to be implemented somewhat public:

- [ ] separate data and template extraction
- [ ] separate raw data fetching and post-processing (add rev and some json magic)
- [ ] provide a way to make a spec from raw data and template

The last point is culmination. The viewer scenario of using it is:
- parse repo, save raw plot data and templates (as text) to DB.
- on user request fetch data for selected commits and pass it to some template (postprocessing happens somewhere inbetween or within a template part) 

Ideally one won't need post-processing or if that is not possible it might be separated into postread and prerender parts. A thing to discuss.

This is not p0, since I'll be handling it in some dirty way for now myself, but still important. 

**NOTE:** this issue is scoped to plot data reading and plot rendering, plot data file discovery is discussed separately in #3807.

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},
     )

pip install -e .[all]
parse_log_pytest
pytest
pytest pytest-cov pytest-xdist pytest-mock pytest-lazy-fixture flaky mock xmltodict awscli google-compute-engine Pygments collective.checkdocs flake8 psutil flake8-docstrings pydocstyle jaraco.windows mock-ssh-server moto rangehttpserver beautifulsoup4 flake8-bugbear flake8-comprehensions flake8-string-format pylint pylint-pytest pylint-plugin-utils wget filelock
apt-get update apt-get install -y gcc
3.7
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
aliyun-python-sdk-core==2.16.0
aliyun-python-sdk-core-v3==2.13.33
aliyun-python-sdk-kms==2.16.5
appdirs==1.4.4
astroid==2.15.8
atpublic==3.1.2
attrs @ file:///croot/attrs_1668696182826/work
autocommand==2.2.2
awscli==1.31.13
azure-common==1.1.28
azure-storage-blob==2.1.0
azure-storage-common==2.1.0
bcrypt==4.2.1
beautifulsoup4==4.13.3
boto==2.49.0
boto3==1.33.13
botocore==1.33.13
cachetools==4.2.4
certifi @ file:///croot/certifi_1671487769961/work/certifi
cffi==1.15.1
charset-normalizer==3.4.1
collective.checkdocs==0.2
colorama==0.4.4
configobj==5.0.9
coverage==7.2.7
crcmod==1.7
cryptography==44.0.2
decorator==5.1.1
dill==0.3.7
distro==1.9.0
docutils==0.16
dpath==2.2.0
-e git+https://github.com/iterative/dvc.git@56833387ce2cbea3b07e9343a2850dce4d8d3e37#egg=dvc
execnet==2.0.2
filelock==3.12.2
flake8==5.0.4
flake8-bugbear==23.3.12
flake8-comprehensions==3.13.0
flake8-docstrings==1.7.0
flake8-string-format==0.3.0
flaky==3.8.1
flatten-json==0.1.14
flit_core @ file:///opt/conda/conda-bld/flit-core_1644941570762/work/source/flit_core
flufl.lock==7.1.1
funcy==2.0
future==1.0.0
gitdb==4.0.12
GitPython==3.1.44
google-api-core==2.10.2
google-api-python-client==2.166.0
google-auth==1.35.0
google-auth-httplib2==0.2.0
google-cloud-core==1.7.3
google-cloud-storage==1.19.0
google-compute-engine==2.8.13
google-crc32c==1.5.0
google-resumable-media==2.7.2
googleapis-common-protos==1.69.2
grandalf==0.6
httplib2==0.22.0
idna==3.10
importlib-metadata==4.2.0
importlib-resources==5.12.0
inflect==6.0.5
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
isort==5.11.5
jaraco.classes==3.2.3
jaraco.collections==4.2.0
jaraco.context==4.3.0
jaraco.functools==3.7.0
jaraco.structures==2.1.0
jaraco.text==3.11.1
jaraco.ui==2.3.0
jaraco.windows==5.7.0
Jinja2==3.1.6
jmespath==0.10.0
jsonpath-ng==1.7.0
lazy-object-proxy==1.9.0
MarkupSafe==2.1.5
mccabe==0.7.0
mock==5.2.0
mock-ssh-server==0.9.1
more-itertools==9.1.0
moto==4.2.14
nanotime==0.5.2
networkx==2.3
numpy==1.21.6
oauth2client==4.1.3
oss2==2.6.1
packaging @ file:///croot/packaging_1671697413597/work
paramiko==3.5.1
path==16.6.0
pathspec==0.11.2
platformdirs==4.0.0
pluggy @ file:///tmp/build/80754af9/pluggy_1648042572264/work
ply==3.11
protobuf==4.24.4
psutil==7.0.0
py @ file:///opt/conda/conda-bld/py_1644396412707/work
pyarrow==12.0.1
pyasn1==0.5.1
pyasn1-modules==0.3.0
pycodestyle==2.9.1
pycparser==2.21
pycryptodome==3.22.0
pydantic==1.10.21
pydocstyle==6.3.0
pydot==2.0.0
PyDrive2==1.15.4
pyflakes==2.5.0
Pygments==2.17.2
pygtrie==2.3.2
pylint==2.17.7
pylint-plugin-utils==0.8.2
pylint-pytest==1.1.8
PyNaCl==1.5.0
pyOpenSSL==25.0.0
pyparsing==3.1.4
pytest==7.1.2
pytest-cov==4.1.0
pytest-lazy-fixture==0.6.3
pytest-mock==3.11.1
pytest-xdist==3.5.0
python-dateutil==2.9.0.post0
PyYAML==5.3.1
rangehttpserver==1.4.0
requests==2.31.0
responses==0.23.3
rsa==4.7.2
ruamel.yaml==0.18.10
ruamel.yaml.clib==0.2.8
s3transfer==0.8.2
shortuuid==1.0.13
six==1.17.0
smmap==5.0.2
snowballstemmer==2.2.0
soupsieve==2.4.1
tabulate==0.9.0
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work
tomlkit==0.12.5
tqdm==4.67.1
typed-ast==1.5.5
types-PyYAML==6.0.12.12
typing_extensions==4.7.1
uritemplate==4.1.1
urllib3==1.26.20
voluptuous==0.14.1
Werkzeug==2.2.3
wget==3.2
wrapt==1.16.0
xmltodict==0.14.2
zc.lockfile==3.0.post1
zipp @ file:///croot/zipp_1672387121353/work

name: dvc
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - attrs=22.1.0=py37h06a4308_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - flit-core=3.6.0=pyhd3eb1b0_0
  - importlib_metadata=4.11.3=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=22.0=py37h06a4308_0
  - pip=22.3.1=py37h06a4308_0
  - pluggy=1.0.0=py37h06a4308_1
  - py=1.11.0=pyhd3eb1b0_0
  - pytest=7.1.2=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tomli=2.0.1=py37h06a4308_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.11.0=py37h06a4308_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - aliyun-python-sdk-core==2.16.0
      - aliyun-python-sdk-core-v3==2.13.33
      - aliyun-python-sdk-kms==2.16.5
      - appdirs==1.4.4
      - astroid==2.15.8
      - atpublic==3.1.2
      - autocommand==2.2.2
      - awscli==1.31.13
      - azure-common==1.1.28
      - azure-storage-blob==2.1.0
      - azure-storage-common==2.1.0
      - bcrypt==4.2.1
      - beautifulsoup4==4.13.3
      - boto==2.49.0
      - boto3==1.33.13
      - botocore==1.33.13
      - cachetools==4.2.4
      - cffi==1.15.1
      - charset-normalizer==3.4.1
      - collective-checkdocs==0.2
      - colorama==0.4.4
      - configobj==5.0.9
      - coverage==7.2.7
      - crcmod==1.7
      - cryptography==44.0.2
      - decorator==5.1.1
      - dill==0.3.7
      - distro==1.9.0
      - docutils==0.16
      - dpath==2.2.0
      - dvc==1.0.0a10+568333
      - execnet==2.0.2
      - filelock==3.12.2
      - flake8==5.0.4
      - flake8-bugbear==23.3.12
      - flake8-comprehensions==3.13.0
      - flake8-docstrings==1.7.0
      - flake8-string-format==0.3.0
      - flaky==3.8.1
      - flatten-json==0.1.14
      - flufl-lock==7.1.1
      - funcy==2.0
      - future==1.0.0
      - gitdb==4.0.12
      - gitpython==3.1.44
      - google-api-core==2.10.2
      - google-api-python-client==2.166.0
      - google-auth==1.35.0
      - google-auth-httplib2==0.2.0
      - google-cloud-core==1.7.3
      - google-cloud-storage==1.19.0
      - google-compute-engine==2.8.13
      - google-crc32c==1.5.0
      - google-resumable-media==2.7.2
      - googleapis-common-protos==1.69.2
      - grandalf==0.6
      - httplib2==0.22.0
      - idna==3.10
      - importlib-metadata==4.2.0
      - importlib-resources==5.12.0
      - inflect==6.0.5
      - isort==5.11.5
      - jaraco-classes==3.2.3
      - jaraco-collections==4.2.0
      - jaraco-context==4.3.0
      - jaraco-functools==3.7.0
      - jaraco-structures==2.1.0
      - jaraco-text==3.11.1
      - jaraco-ui==2.3.0
      - jaraco-windows==5.7.0
      - jinja2==3.1.6
      - jmespath==0.10.0
      - jsonpath-ng==1.7.0
      - lazy-object-proxy==1.9.0
      - markupsafe==2.1.5
      - mccabe==0.7.0
      - mock==5.2.0
      - mock-ssh-server==0.9.1
      - more-itertools==9.1.0
      - moto==4.2.14
      - nanotime==0.5.2
      - networkx==2.3
      - numpy==1.21.6
      - oauth2client==4.1.3
      - oss2==2.6.1
      - paramiko==3.5.1
      - path==16.6.0
      - pathspec==0.11.2
      - platformdirs==4.0.0
      - ply==3.11
      - protobuf==4.24.4
      - psutil==7.0.0
      - pyarrow==12.0.1
      - pyasn1==0.5.1
      - pyasn1-modules==0.3.0
      - pycodestyle==2.9.1
      - pycparser==2.21
      - pycryptodome==3.22.0
      - pydantic==1.10.21
      - pydocstyle==6.3.0
      - pydot==2.0.0
      - pydrive2==1.15.4
      - pyflakes==2.5.0
      - pygments==2.17.2
      - pygtrie==2.3.2
      - pylint==2.17.7
      - pylint-plugin-utils==0.8.2
      - pylint-pytest==1.1.8
      - pynacl==1.5.0
      - pyopenssl==25.0.0
      - pyparsing==3.1.4
      - pytest-cov==4.1.0
      - pytest-lazy-fixture==0.6.3
      - pytest-mock==3.11.1
      - pytest-xdist==3.5.0
      - python-dateutil==2.9.0.post0
      - pyyaml==5.3.1
      - rangehttpserver==1.4.0
      - requests==2.31.0
      - responses==0.23.3
      - rsa==4.7.2
      - ruamel-yaml==0.18.10
      - ruamel-yaml-clib==0.2.8
      - s3transfer==0.8.2
      - shortuuid==1.0.13
      - six==1.17.0
      - smmap==5.0.2
      - snowballstemmer==2.2.0
      - soupsieve==2.4.1
      - tabulate==0.9.0
      - tomlkit==0.12.5
      - tqdm==4.67.1
      - typed-ast==1.5.5
      - types-pyyaml==6.0.12.12
      - typing-extensions==4.7.1
      - uritemplate==4.1.1
      - urllib3==1.26.20
      - voluptuous==0.14.1
      - werkzeug==2.2.3
      - wget==3.2
      - wrapt==1.16.0
      - xmltodict==0.14.2
      - zc-lockfile==3.0.post1
prefix: /opt/conda/envs/dvc

tests/func/plots/test_diff.py::test_diff_dirty
tests/func/plots/test_plots.py::test_plot_csv_one_column tests/func/plots/test_plots.py::test_plot_csv_multiple_columns tests/func/plots/test_plots.py::test_plot_csv_choose_axes tests/func/plots/test_plots.py::test_plot_json_single_val tests/func/plots/test_plots.py::test_plot_json_multiple_val tests/func/plots/test_plots.py::test_plot_confusion tests/func/plots/test_plots.py::test_plot_multiple_revs_default tests/func/plots/test_plots.py::test_plot_multiple_revs tests/func/plots/test_plots.py::test_plot_even_if_metric_missing tests/func/plots/test_plots.py::test_throw_on_no_metric_at_all tests/func/plots/test_plots.py::test_custom_template tests/func/plots/test_plots.py::test_no_plots tests/func/plots/test_plots.py::test_should_raise_on_no_template tests/func/plots/test_plots.py::test_plot_wrong_metric_type tests/func/plots/test_plots.py::test_plot_choose_columns tests/func/plots/test_plots.py::test_plot_default_choose_column tests/func/plots/test_plots.py::test_plot_yaml tests/func/plots/test_plots.py::test_raise_on_wrong_field tests/func/plots/test_plots.py::test_load_metric_from_dict_json tests/func/plots/test_plots.py::test_load_metric_from_dict_yaml tests/unit/command/test_plots.py::test_metrics_diff tests/unit/command/test_plots.py::test_metrics_show tests/unit/command/test_plots.py::test_plots_show_vega
dvc/repo/plots/__init__.py dvc/repo/plots/template.py dvc/repo/plots/show.py dvc/repo/plots/data.py
dvc/repo/plots/__init__.py dvc/repo/plots/template.py dvc/repo/plots/show.py dvc/repo/plots/data.py