Draco Debugging#

%pip install -q toml ipywidgets
Note: you may need to restart the kernel to use updated packages.
import toml
from draco import Draco
from draco.debug import DracoDebug, DracoDebugPlotter, DracoDebugChartConfig

Why doesn’t Draco produce what I want?#

There are several reasons:

First, the generator in generate.lp may not be capable of enumerate every possibility. Then, the answer you have in mind might also have been left out by hard constraints. An extreme situation you might observe is that Draco returns an empty result. If you have edge cases you want to test on, you can add constraints temporarily to check if Draco can still generate answers. For how to do this, refer to How to modify the knowledge-base with constraints. Then you can extend the generator or revise the hard constraints accordingly after making sure your testing cases are well-defined.

If you have pinpointed the issue to the hard constraints collection, you might want to take a step to relax the hard constraints for further investigation before you make any changes. For how to do this, refer to Debugging empty results.

Second, the answer might have a low ranking so that Draco promotes other answers instead. This can be caused by lack of proper soft constraints, incorrectly-defined soft constraints, or unsuitable constraint weights. To make sure the soft constraints are well-defined, add more unit tests and refer to Validating the soft constraints with full specifications. For soft constraint introspection, check out How to modify the knowledge-base with constraints.

In general, here is a guideline for what to do:

  • If you see that there are too many recommendations, you can:

    • add more hard constraints

    • modify your generator and hard constraints to reduce symmetry in the search space (e.g. similar recommendations with switched entity ids)

  • If you see too few recommendations, you can:

    • check if some of your constraints are too tight, and move them to soft constraints

  • If you see no recommendations, you might have made mistakes in the hard constraints. You can allow violations to check what are the common ones by removing the violation constraint, which forbids any violations, from the programs.

Validating the soft constraints with full specifications#

Although we ensure 100% testing coverage including all soft constraints, we might want to assess and validate that the soft constraint collection carries out the intended goal in real-world visualization designs.

Here we demonstrate how we validate the soft constraints violated in specific examples.

Loading Specifications#

Specifications to debug can be declared as a dictionary or can be loaded from a static file. The debugger will generate a dataframe from your input automatically, ready to be analyzed!

example_specs = toml.load(open("./data/example_charts.toml"))
default_draco = Draco()

debugger = DracoDebug(specs=example_specs, draco=default_draco)
debugger.chart_preferences.head()
chart_name pref_name pref_description count weight
0 tick_plot linear_x Linear scale with x channel. 1 0
1 tick_plot c_d_overlap_tick Continuous by discrete for tick mark. 1 0
2 tick_plot linear_scale linear scale. 1 0
3 tick_plot continuous_pos_not_zero Prefer zero continuous positional. 1 1
4 tick_plot continuous_not_zero Prefer to include zero for continuous (binned ... 1 1

Visualizing Debug Data#

The chart_preferences dataframe generated by DracoDebug can be visualized using DracoDebugPlotter. Custom plot configurations can be passed to DracoDebugPlotter.create_chart to customize the produced visualization.

plotter = DracoDebugPlotter(debugger.chart_preferences)
# Creates a chart using the default config (alphabetical sort)
plotter.create_chart()

Interactive Selection of Debugging Variants#

Interactions will work only in a Python-enabled environment! Please clone the repository or start it in Binder.

import ipywidgets as widgets

config_title_dropdown = widgets.Dropdown(
    options=[cfg.value.title for cfg in DracoDebugChartConfig],
    value=DracoDebugChartConfig.SORT_ALPHABETICALLY.value.title,
    description="Sorting:",
    disabled=False,
)
violated_prefs_only_checkbox = widgets.Checkbox(
    value=False,
    description="Show violated preferences only",
    disabled=False,
)


def create_size_slider(value: float, description: str):
    return widgets.FloatSlider(
        value=value,
        min=100,
        max=1800,
        step=10,
        description=description,
        disabled=False,
        continuous_update=False,
        orientation="horizontal",
        readout=True,
        readout_format=".1f",
    )


size_slider_width = create_size_slider(value=1000, description="Width:")
size_slider_height = create_size_slider(value=400, description="Height:")


def on_config_change(
    config_title: str, violated_prefs_only: bool, width: float, height: float
):
    # Creating new instances to avoid collisions with other variables in this notebook
    dbg = DracoDebug(specs=example_specs, draco=default_draco)
    plt = DracoDebugPlotter(dbg.chart_preferences)
    chart = plt.create_chart(
        cfg=DracoDebugChartConfig.by_title(config_title),
        violated_prefs_only=violated_prefs_only,
        plot_size=(width, height),
    )
    display(chart)


out = widgets.interactive_output(
    on_config_change,
    {
        "config_title": config_title_dropdown,
        "violated_prefs_only": violated_prefs_only_checkbox,
        "width": size_slider_width,
        "height": size_slider_height,
    },
)
controls = widgets.HBox(
    [
        config_title_dropdown,
        violated_prefs_only_checkbox,
        widgets.VBox([size_slider_width, size_slider_height]),
    ]
)
widgets.VBox([controls, out])

With the heatmap above, we can verify if each soft constraint expresses the intended rule under the context of visualization designs. In the collection above, there are groups of visualizations that are more relevant to each other in terms of their specification. For example, knowing that the most significant difference between a bubble chart and a colored scatterplot is that a bubble chart has a quantitative size channel while a colored scatterplot has a categorical color channel, one can verify that the relevant constraints are violated accordingly (i.e., categorical_scale and categorical_color for the colored scatterplot, and the size_not_zero and linear_size for the bubble chart).

In addition, the heatmap indicates the coverage of violations in the chosen example. We can then incrementally add new examples that focus on constraints with low/zero coverage.

On the other hand, when we identify that the existing constraints saturated for substantially different example, we might be able to uncover new constraints to extend the knowledge space.

How to modify the knowledge base with constraints#

You can design your own description language and use it with Draco or extend the existing language we use here. If you don’t know where to start with the constraints, you can first use our Draco APIs to generate some recommendations. Then, you should be able to find some recommendations that should have been left out, and you can write constraints to reflect them. If you write your own description language, you need to set up the search space in a similar way to generate.lp before trying to generate recommendations.

For example, the following snippet shows how to generate 5 recommendation. You can set a different number to look into more results.

from draco import Draco, answer_set_to_dict, dict_to_facts, schema_from_dataframe
from pprint import pprint


d = Draco()
from draco.renderer import AltairRenderer
from vega_datasets import data
from IPython.display import display
import warnings

# Suppressing warnings raised by altair in the background
# (iteration-related deprecation warnings)
warnings.filterwarnings("ignore")

# Setting up renderer and demo data
renderer = AltairRenderer()
weather_data = data.seattle_weather()

# Generating facts about data
weather_schema = schema_from_dataframe(weather_data)
weather_facts = dict_to_facts(weather_schema)
input_spec = weather_facts + [
    "attribute(task,root,summary).",
    "entity(view,root,(v,0)).",
    "entity(mark,(v,0),(m,0)).",
    "entity(encoding,(m,0),(e,0)).",
    "attribute((encoding,field),(e,0),temp_max).",
    "entity(encoding,(m,0),(e,1)).",
    "attribute((encoding,field),(e,1),precipitation).",
    "#show entity/3.",
    "#show attribute/3.",
]

print("INPUT:")
print(input_spec)

print("OUTPUT:")

specs = {}

for i, model in enumerate(d.complete_spec(input_spec, 5)):
    chart_num = i + 1
    spec = answer_set_to_dict(model.answer_set)
    chart_name = f"Rec {chart_num}"
    specs[chart_name] = dict_to_facts(spec)
    print(f"CHART {chart_num}")
    print(f"COST: {model.cost}")
    pprint(spec)
    display(renderer.render(spec=spec, data=weather_data))

print("VIOLATED PREFERENCES:")
debugger = DracoDebug(specs=specs, draco=default_draco)
chart_preferences = debugger.chart_preferences
plotter = DracoDebugPlotter(chart_preferences)

# sort by sum of count
plotter.create_chart(cfg=DracoDebugChartConfig.SORT_BY_COUNT_SUM)
INPUT:
['attribute(number_rows,root,1461).', 'entity(field,root,0).', 'attribute((field,name),0,date).', 'attribute((field,type),0,datetime).', 'attribute((field,unique),0,1461).', 'attribute((field,entropy),0,7287).', 'entity(field,root,1).', 'attribute((field,name),1,precipitation).', 'attribute((field,type),1,number).', 'attribute((field,unique),1,111).', 'attribute((field,entropy),1,2422).', 'attribute((field,min),1,0).', 'attribute((field,max),1,55).', 'attribute((field,std),1,6).', 'entity(field,root,2).', 'attribute((field,name),2,temp_max).', 'attribute((field,type),2,number).', 'attribute((field,unique),2,67).', 'attribute((field,entropy),2,3934).', 'attribute((field,min),2,-1).', 'attribute((field,max),2,35).', 'attribute((field,std),2,7).', 'entity(field,root,3).', 'attribute((field,name),3,temp_min).', 'attribute((field,type),3,number).', 'attribute((field,unique),3,55).', 'attribute((field,entropy),3,3596).', 'attribute((field,min),3,-7).', 'attribute((field,max),3,18).', 'attribute((field,std),3,5).', 'entity(field,root,4).', 'attribute((field,name),4,wind).', 'attribute((field,type),4,number).', 'attribute((field,unique),4,79).', 'attribute((field,entropy),4,3950).', 'attribute((field,min),4,0).', 'attribute((field,max),4,9).', 'attribute((field,std),4,1).', 'entity(field,root,5).', 'attribute((field,name),5,weather).', 'attribute((field,type),5,string).', 'attribute((field,unique),5,5).', 'attribute((field,entropy),5,1201).', 'attribute((field,freq),5,714).', 'attribute(task,root,summary).', 'entity(view,root,(v,0)).', 'entity(mark,(v,0),(m,0)).', 'entity(encoding,(m,0),(e,0)).', 'attribute((encoding,field),(e,0),temp_max).', 'entity(encoding,(m,0),(e,1)).', 'attribute((encoding,field),(e,1),precipitation).', '#show entity/3.', '#show attribute/3.']
OUTPUT:
CHART 1
COST: [14]
{'field': [{'entropy': 7287,
            'name': 'date',
            'type': 'datetime',
            'unique': 1461},
           {'entropy': 2422,
            'max': 55,
            'min': 0,
            'name': 'precipitation',
            'std': 6,
            'type': 'number',
            'unique': 111},
           {'entropy': 3934,
            'max': 35,
            'min': -1,
            'name': 'temp_max',
            'std': 7,
            'type': 'number',
            'unique': 67},
           {'entropy': 3596,
            'max': 18,
            'min': -7,
            'name': 'temp_min',
            'std': 5,
            'type': 'number',
            'unique': 55},
           {'entropy': 3950,
            'max': 9,
            'min': 0,
            'name': 'wind',
            'std': 1,
            'type': 'number',
            'unique': 79},
           {'entropy': 1201,
            'freq': 714,
            'name': 'weather',
            'type': 'string',
            'unique': 5}],
 'number_rows': 1461,
 'task': 'summary',
 'view': [{'coordinates': 'cartesian',
           'mark': [{'encoding': [{'channel': 'y', 'field': 'temp_max'},
                                  {'channel': 'x', 'field': 'precipitation'}],
                     'type': 'point'}],
           'scale': [{'channel': 'y', 'type': 'linear'},
                     {'channel': 'x', 'type': 'linear', 'zero': 'true'}]}]}
CHART 2
COST: [14]
{'field': [{'entropy': 7287,
            'name': 'date',
            'type': 'datetime',
            'unique': 1461},
           {'entropy': 2422,
            'max': 55,
            'min': 0,
            'name': 'precipitation',
            'std': 6,
            'type': 'number',
            'unique': 111},
           {'entropy': 3934,
            'max': 35,
            'min': -1,
            'name': 'temp_max',
            'std': 7,
            'type': 'number',
            'unique': 67},
           {'entropy': 3596,
            'max': 18,
            'min': -7,
            'name': 'temp_min',
            'std': 5,
            'type': 'number',
            'unique': 55},
           {'entropy': 3950,
            'max': 9,
            'min': 0,
            'name': 'wind',
            'std': 1,
            'type': 'number',
            'unique': 79},
           {'entropy': 1201,
            'freq': 714,
            'name': 'weather',
            'type': 'string',
            'unique': 5}],
 'number_rows': 1461,
 'task': 'summary',
 'view': [{'coordinates': 'cartesian',
           'mark': [{'encoding': [{'binning': 10,
                                   'channel': 'y',
                                   'field': 'temp_max'},
                                  {'channel': 'x', 'field': 'precipitation'}],
                     'type': 'tick'}],
           'scale': [{'channel': 'y', 'type': 'linear'},
                     {'channel': 'x', 'type': 'linear', 'zero': 'true'}]}]}
CHART 3
COST: [14]
{'field': [{'entropy': 7287,
            'name': 'date',
            'type': 'datetime',
            'unique': 1461},
           {'entropy': 2422,
            'max': 55,
            'min': 0,
            'name': 'precipitation',
            'std': 6,
            'type': 'number',
            'unique': 111},
           {'entropy': 3934,
            'max': 35,
            'min': -1,
            'name': 'temp_max',
            'std': 7,
            'type': 'number',
            'unique': 67},
           {'entropy': 3596,
            'max': 18,
            'min': -7,
            'name': 'temp_min',
            'std': 5,
            'type': 'number',
            'unique': 55},
           {'entropy': 3950,
            'max': 9,
            'min': 0,
            'name': 'wind',
            'std': 1,
            'type': 'number',
            'unique': 79},
           {'entropy': 1201,
            'freq': 714,
            'name': 'weather',
            'type': 'string',
            'unique': 5}],
 'number_rows': 1461,
 'task': 'summary',
 'view': [{'coordinates': 'cartesian',
           'mark': [{'encoding': [{'channel': 'x', 'field': 'temp_max'},
                                  {'channel': 'y', 'field': 'precipitation'}],
                     'type': 'point'}],
           'scale': [{'channel': 'x', 'type': 'linear'},
                     {'channel': 'y', 'type': 'linear', 'zero': 'true'}]}]}
CHART 4
COST: [14]
{'field': [{'entropy': 7287,
            'name': 'date',
            'type': 'datetime',
            'unique': 1461},
           {'entropy': 2422,
            'max': 55,
            'min': 0,
            'name': 'precipitation',
            'std': 6,
            'type': 'number',
            'unique': 111},
           {'entropy': 3934,
            'max': 35,
            'min': -1,
            'name': 'temp_max',
            'std': 7,
            'type': 'number',
            'unique': 67},
           {'entropy': 3596,
            'max': 18,
            'min': -7,
            'name': 'temp_min',
            'std': 5,
            'type': 'number',
            'unique': 55},
           {'entropy': 3950,
            'max': 9,
            'min': 0,
            'name': 'wind',
            'std': 1,
            'type': 'number',
            'unique': 79},
           {'entropy': 1201,
            'freq': 714,
            'name': 'weather',
            'type': 'string',
            'unique': 5}],
 'number_rows': 1461,
 'task': 'summary',
 'view': [{'coordinates': 'cartesian',
           'mark': [{'encoding': [{'binning': 10,
                                   'channel': 'x',
                                   'field': 'temp_max'},
                                  {'channel': 'y', 'field': 'precipitation'}],
                     'type': 'tick'}],
           'scale': [{'channel': 'x', 'type': 'linear'},
                     {'channel': 'y', 'type': 'linear', 'zero': 'true'}]}]}
CHART 5
COST: [16]
{'field': [{'entropy': 7287,
            'name': 'date',
            'type': 'datetime',
            'unique': 1461},
           {'entropy': 2422,
            'max': 55,
            'min': 0,
            'name': 'precipitation',
            'std': 6,
            'type': 'number',
            'unique': 111},
           {'entropy': 3934,
            'max': 35,
            'min': -1,
            'name': 'temp_max',
            'std': 7,
            'type': 'number',
            'unique': 67},
           {'entropy': 3596,
            'max': 18,
            'min': -7,
            'name': 'temp_min',
            'std': 5,
            'type': 'number',
            'unique': 55},
           {'entropy': 3950,
            'max': 9,
            'min': 0,
            'name': 'wind',
            'std': 1,
            'type': 'number',
            'unique': 79},
           {'entropy': 1201,
            'freq': 714,
            'name': 'weather',
            'type': 'string',
            'unique': 5}],
 'number_rows': 1461,
 'task': 'summary',
 'view': [{'coordinates': 'cartesian',
           'mark': [{'encoding': [{'channel': 'x', 'field': 'temp_max'},
                                  {'binning': 10,
                                   'channel': 'y',
                                   'field': 'precipitation'}],
                     'type': 'tick'}],
           'scale': [{'channel': 'x', 'type': 'linear'},
                     {'channel': 'y', 'type': 'linear', 'zero': 'true'}]}]}
VIOLATED PREFERENCES:

We can add hard constraints temporarily to further filter and inspect the answer set.

Here we exclude the multi-layer or multi-view designs, and we ensure the recommendations having at least 3 encodings.

# exclude multi-layer or multi-view designs.
# filter out designs with less than 3 encodings
input_spec = [
    "attribute(task,root,summary).",
    "attribute(number_rows,root,100).",
    "entity(field,root,temp_max).",
    "attribute((field,name),temp_max,temp_max).",
    "attribute((field,type),temp_max,number).",
    "attribute((field,unique),temp_max,100).",
    "attribute((field,min),temp_max,-1).", 
    "attribute((field,max),temp_max,35).",
    "entity(field,root,f1).",
    "attribute((field,name),f1,precipitation).",
    "attribute((field,type),f1,number).",
    "entity(view,root,(v,0)).",
    "entity(mark,(v,0),(m,0)).",
    "entity(encoding,(m,0),(e,0)).",
    "attribute((encoding,field),(e,0),temp_max).",
    "entity(encoding,(m,0),(e,1)).",
    "attribute((encoding,field),(e,1),precipitation).",
    "#show entity/3.",
    "#show attribute/3.",
]

new_input = input_spec + [
    ":- {entity(encoding,_,_)} <= 2.",
    ":- {entity(mark,_,_)} >= 2.",
]

We will demonstrate how we found a bug in one of our soft constraints and fixed it. At the beginning, the soft constraint binned_orientation_not_x was incorrectly defined as:

preference(binned_orientation_not_x,E) :- 
    attribute((field,type),F,(number;datetime)), 
    helper((encoding,field),E,F), 
    attribute((encoding,binning),E,_), 
    not attribute((encoding,channel),E,x).

The reason why this is incorrect is explained below, after the charts in this section.

Now, to simulate our debugging process, let’s replace the now correct definition with the incorrect version:

from draco.asp_utils import blocks_to_program
from draco.programs import soft

# remove the correct definition, and then replace it with the incorrect one.
s = "".join(
    blocks_to_program(
        soft.blocks, set(soft.blocks.keys()) - {"binned_orientation_not_x"}
    )
)

s += """preference(binned_orientation_not_x,E) :-
    attribute((field,type),F,(number;datetime)),
    helper((encoding,field),E,F),
    attribute((encoding,binning),E,_),
    not attribute((encoding,channel),E,x)."""

new_draco = Draco(soft=s)

specs = {}
for i, model in enumerate(new_draco.complete_spec(new_input, 3)):
    chart_num = i + 1
    spec = answer_set_to_dict(model.answer_set)
    chart_name = f"Rec {chart_num}"
    specs[chart_name] = dict_to_facts(spec)
    print(f"CHART {chart_num}")
    print(f"COST: {model.cost}")
    pprint(spec)
    display(renderer.render(spec=spec, data=weather_data))

print("VIOLATED PREFERENCES:")
debugger = DracoDebug(specs=specs, draco=new_draco)
plotter = DracoDebugPlotter(debugger.chart_preferences)

# sort by sum of count
plotter.create_chart(cfg=DracoDebugChartConfig.SORT_BY_COUNT_SUM)
CHART 1
COST: [18]
{'field': [{'max': 35,
            'min': -1,
            'name': 'temp_max',
            'type': 'number',
            'unique': 100},
           {'name': 'precipitation', 'type': 'number'}],
 'number_rows': 100,
 'task': 'summary',
 'view': [{'coordinates': 'cartesian',
           'mark': [{'encoding': [{'binning': 10,
                                   'channel': 'x',
                                   'field': 'temp_max'},
                                  {'channel': 'y', 'field': 'precipitation'},
                                  {'aggregate': 'count', 'channel': 'size'}],
                     'type': 'point'}],
           'scale': [{'channel': 'x', 'type': 'linear'},
                     {'channel': 'y', 'type': 'ordinal'},
                     {'channel': 'size', 'type': 'linear', 'zero': 'true'}]}]}
CHART 2
COST: [19]
{'field': [{'max': 35,
            'min': -1,
            'name': 'temp_max',
            'type': 'number',
            'unique': 100},
           {'name': 'precipitation', 'type': 'number'}],
 'number_rows': 100,
 'task': 'summary',
 'view': [{'coordinates': 'cartesian',
           'mark': [{'encoding': [{'binning': 10,
                                   'channel': 'y',
                                   'field': 'temp_max'},
                                  {'binning': 10,
                                   'channel': 'x',
                                   'field': 'precipitation'},
                                  {'aggregate': 'count', 'channel': 'size'}],
                     'type': 'point'}],
           'scale': [{'channel': 'size', 'type': 'linear', 'zero': 'true'},
                     {'channel': 'x', 'type': 'linear'},
                     {'channel': 'y', 'type': 'linear'}]}]}
CHART 3
COST: [19]
{'field': [{'max': 35,
            'min': -1,
            'name': 'temp_max',
            'type': 'number',
            'unique': 100},
           {'name': 'precipitation', 'type': 'number'}],
 'number_rows': 100,
 'task': 'summary',
 'view': [{'coordinates': 'cartesian',
           'mark': [{'encoding': [{'binning': 10,
                                   'channel': 'y',
                                   'field': 'temp_max'},
                                  {'binning': 10,
                                   'channel': 'x',
                                   'field': 'precipitation'},
                                  {'aggregate': 'count', 'channel': 'size'}],
                     'type': 'point'}],
           'scale': [{'channel': 'size', 'type': 'linear', 'zero': 'true'},
                     {'channel': 'x', 'type': 'linear', 'zero': 'true'},
                     {'channel': 'y', 'type': 'linear'}]}]}
VIOLATED PREFERENCES:

Note that the first chart shouldn’t be preferred to the latter two because the precipitation field with floating numbers shouldn’t be treated as discrete.

The difference of the scores results from the bin, ordinal_scale and binned_orientation_not_x since they are all non-zero. Then with bin and ordinal_scale having the same count and weight, the only reason CHART 2 and 3 being ranked lower was the binned_oriented_not_x. However, CHART 2 and 3 both do have binned x-axis. Realizing that binned_oriented_not_x should have meant that “Prefer binned quantitative on x-axis if y-axis is not binned”, we fixed the soft constraint definition in soft.lp as:

preference(binned_orientation_not_x,E) :- 
    attribute((field,type),F,(number;datetime)), 
    helper((encoding,field),E,F), 
    attribute((encoding,binning),E,_), 
    not attribute((encoding,channel),_,x).
d.count_preferences(specs["Rec 2"])
defaultdict(int,
            {'cartesian_coordinate': 1,
             'summary_point': 1,
             'aggregate_count': 1,
             'linear_size': 1,
             'linear_y': 1,
             'linear_x': 1,
             'd_d_point': 1,
             'linear_scale': 3,
             'encoding_field': 2,
             'encoding': 3,
             'bin': 2,
             'aggregate': 1})

Now, the CHART 2 is no longer violating the preference binned_orientation_not_x.

Debugging empty results#

While we are exploring with the knowledge base with partial specifications as input, we may encounter the situation where Draco returns empty results.

If you see too few recommendations, you can check if some of your constraints are too tight, and move them to soft constraints. If you see no recommendations, you might have made mistakes in the hard constraints. You can allow violations to check what are the common ones by removing the violation constraint, which forbids any violations, from the programs. Below is an example:

for model in d.complete_spec(new_input):
    spec = answer_set_to_dict(model.answer_set)
    pprint(spec)

    print("VIOLATED HARD CONSTRAINTS:")
    answer = [str(symbol) + ". " for symbol in model.answer_set]
    print(d.get_violations(answer))
{'field': [{'max': 35,
            'min': -1,
            'name': 'temp_max',
            'type': 'number',
            'unique': 100},
           {'name': 'precipitation', 'type': 'number'}],
 'number_rows': 100,
 'task': 'summary',
 'view': [{'coordinates': 'cartesian',
           'mark': [{'encoding': [{'binning': 10,
                                   'channel': 'x',
                                   'field': 'temp_max'},
                                  {'channel': 'y', 'field': 'precipitation'},
                                  {'aggregate': 'count', 'channel': 'size'}],
                     'type': 'point'}],
           'scale': [{'channel': 'x', 'type': 'linear'},
                     {'channel': 'y', 'type': 'ordinal'},
                     {'channel': 'size', 'type': 'linear', 'zero': 'true'}]}]}
VIOLATED HARD CONSTRAINTS:
[]
# `constraints_no_violation` gives access to the constraints without including the "violation" constraint
new_draco = Draco(constraints=d.constraints_no_violation)

for model in new_draco.complete_spec(new_input):
    spec = answer_set_to_dict(model.answer_set)
    pprint(spec)

    print("VIOLATED HARD CONSTRAINTS:")
    answer = [str(symbol) + ". " for symbol in model.answer_set]
    print(new_draco.get_violations(answer))
{'field': [{'max': 35,
            'min': -1,
            'name': 'temp_max',
            'type': 'number',
            'unique': 100},
           {'name': 'precipitation', 'type': 'number'}],
 'number_rows': 100,
 'task': 'summary',
 'view': [{'coordinates': 'cartesian',
           'mark': [{'encoding': [{'channel': 'detail', 'field': 'temp_max'},
                                  {'channel': 'x', 'field': 'precipitation'},
                                  {'channel': 'x'},
                                  {'channel': 'y'},
                                  {'channel': 'y'},
                                  {'channel': 'x'}],
                     'type': 'rect'}],
           'scale': [{'channel': 'detail', 'type': 'linear'},
                     {'channel': 'x', 'type': 'linear', 'zero': 'true'},
                     {'channel': 'x', 'type': 'linear', 'zero': 'true'}]}]}
VIOLATED HARD CONSTRAINTS:
['enforce_order', 'detail_not_ordinal', 'rect_without_d_d', 'detail_without_agg', 'encoding_no_field_and_not_count', 'encoding_channel_without_scale', 'scale_repeat_channel', 'encoding_repeat_channel']