Skip to content

Conversation

@peter-gy
Copy link
Collaborator

@peter-gy peter-gy commented Jul 21, 2025

CHANGELOG

DracoExpress class acting as a facade over Draco fetaures

  • Reduces boilerplate around spec completion and rendering of recommendations via DracoChartSpec.
  • Facilitates running and comparing completions with partially modified weights (DracoExpress.with_weights) and modified programs (DracoExpress.with_draco). This is really useful for inspecting how a given set of weights and design rules impact recommendations and the API makes it easier to experiment with different configs in a reactive environment like Marimo
  • express.lp provides a simpler ASP API allowing us to construct more terse Draco programs where we want to anchor over a certain field, encoding, mark, etc. This is extremely useful in scenarios where a user (or an LLM) comes up with certain non-negotiable details about a vis (e.g. date, wind and condition fields must be used and condition must be used for faceting) and let Draco complete all the remaining parts of the spec optimally.
  • Load / Dump state makes it easier to experiment with the development of a dedicated interactive weight tuner and debugging environment:
CleanShot 2025-07-21 at 15 11 13@2x

Let's say I use Draco with a specific set of weights and specific ASP core to recommend charts for my dataset. A state dump captures all the required context (data over which recommendations were made, programs formalizing the design guidelines based on which charts are recommended and weights defining chart feature priorities:

[
  {
    "data": [
      {
        "publication_year": "1990-01-01",
        "conference": "Vis",
        "paper_count": 53
      },
      {
        "publication_year": "1991-01-01",
        "conference": "Vis",
        "paper_count": 57
      },
      {
        "publication_year": "1992-01-01",
        "conference": "Vis",
        "paper_count": 59
      },
      ...
    ],
    "programs": [
      {
        "name": "define",
        "src": "% ====== Definitions of valid function domains ======\n\n% @definition(mark_type) Types of marks to encode data.\ndomain((mark,type),(point;bar;line;area;text;tick;rect)).\n\n% @definition(view_coordinate) Types of coordinates.\ndomain((view,coordinates),(cartesian;polar)).\n\n% @definition(field_type) Basic types of the data.\ndomain((field,type),(string;number;boolean;datetime)).\n\n% @definition(field_names) Names of the data.\ndomain((field,name),F) :- attribute((field,name),_,F).\n\n% @definition(encoding_fields) Encoding fields are defined by field names.\ndomain((encoding,field),N) :- attribute((field,name),_,N).\n\n% @definition(aggregate) Aggregation functions.\ndomain((encoding,aggregate),(count;mean;median;min;max;stdev;sum)).\ndomain((encoding,aggregate),(count;sum)).\n\n% @definition(binning) Numbers of bins that can be recommended, any natural number is allowed.\ndomain(binCount,(10;25;200)).\ndomain((encoding,binning),N) :- domain(binCount,N).\n\n% @definition(channel) Encoding channels.\ndomain(positional,(x;y)).\ndomain(non_positional,(color;size;shape;text;detail)).\ndomain(single_channel,C) :- domain(positional,C).\ndomain(single_channel,C) :- domain(non_positional,C).\ndomain(multi_channel,detail).\ndomain(channel,C) :- domain(single_channel,C).\ndomain(channel,C) :- domain(multi_channel,C).\n\ndomain((encoding,channel),C) :- domain(channel,C).\ndomain((scale,channel),C) :- domain(channel,C).\n\n% @definition(encoding_channel) All channels in the mark's encodings.\ndomain(encoding_channel,V,C) :-\n    entity(mark,V,M),\n    entity(encoding,M,E),\n    attribute((encoding,channel),E,C).\n\n% @definition(scale_encoding) All channels in the view's and root's scales.\ndomain(scale_channel,V,C) :-\n    entity(scale,V,S),\n    attribute((scale,channel),S,C).\ndomain(scale_channel,V,C) :-\n    entity(view,R,V),\n    entity(scale,R,S),\n    attribute((scale,channel),S,C).\n\n% @definition(scale_type) Scale types.\ndomain(discrete_scale,(ordinal;categorical)).\ndomain(continuous_scale,(log;symlog;linear)).\ndomain(scale_type,T) :- domain(discrete_scale,T).\ndomain(scale_type,T) :- domain(continuous_scale,T).\n\ndomain((scale,type),T) :- domain(scale_type,T).\n\n% @definition(task) Tasks.\ndomain(task,(value;summary)).\n\n% @definition(stack) Stacking methods.\ndomain((encoding,stack),(zero;center;normalize)).\n\n% @definition(scale_zero) Scale domain includes 0.\ndomain((scale,zero),true).\n\n% @definition(facets) Definition for facets.\ndomain((facet,field),F) :- attribute((field,name),_,F).\ndomain((facet,channel),(row;col)).\ndomain((facet,binning),N) :- domain(binCount,N).\n\n% @definition(interesting) Definition for fields that are relevant to the task.\ndomain((field,interesting),true).\n"
      },
      {
        "name": "constraints",
        "src": "% ====== Constraints ======\n\n% @definition(invalid_domain) Only valid domain values are allowed.\nviolation(invalid_domain) :- attribute(P,_,V), domain(P,_), not domain(P,V).\n\n% @definition(duplicate_attribute) Do not define the same attribute twice.\nviolation(duplicate_attribute) :- attribute(P,O,V1), attribute(P,O,V2), domain(P,_), V1<V2.\n\n% @definition(duplicate_field_name) Do not allow same field names.\nviolation(duplicate_field_name) :- attribute((field,name),F1,N), attribute((field,name),F2,N), F1<F2.\n\n% @definition(valid_fields) Only allow fields that have been defined.\nviolation(existing_field) :- attribute(((encoding;facet),field),_,N), not attribute((field,name),_,N).\n\n% @definition(attribute_entity) The type of an attribute path should link to the correct entity.\nviolation(attribute_entity) :- attribute((P,_),E,_), entity(_,_,E), not entity(P,_,E).\n\n% @definition(violation) No violations allowed.\n:- violation(_).\n"
      },
      {
        "name": "helpers",
        "src": "% ====== Helpers ======\n\n% @helper(mark_channel) Whether a mark has an encoding for a channel.\nhelper((mark,channel),M,C) :-\n    entity(encoding,M,E),\n    attribute((encoding,channel),E,C).\n\n% @helper(mark_with_stack) Whether a mark has an encoding with stacking.\nhelper(mark_with_stack,M) :-\n    entity(encoding,M,E),\n    attribute((encoding,stack),E,_).\n\n% @helper(mark_scale) Whether a mark has a scale.\nhelper((mark,scale),M,S) :-\n    entity(mark,V,M),\n    entity(scale,V,S).\nhelper((mark,scale),M,S) :-\n    entity(view,R,V),\n    entity(mark,V,M),\n    entity(scale,R,S).\n\n% @helper(mark_scale_channel) Whether a mark has a scale type for a channel.\nhelper(mark_scale_channel,M,T,C) :-\n    helper((mark,scale),M,S),\n    attribute((scale,channel),S,C),\n    attribute((scale,type),S,T).\n\n% @helper(mark_encoding_scale) Link mark, encoding, and scale together.\nhelper(mark_encoding_scale,M,E,S) :-\n    entity(encoding,M,E),\n    attribute((encoding,channel),E,C),\n    helper((mark,scale),M,S),\n    attribute((scale,channel),S,C).\n\n% @helper(encoding_type) Whether an encoding has a channel for a scale type.\nhelper((encoding,scale_type),E,T) :-\n    entity(encoding,M,E),\n    attribute((encoding,channel),E,C),\n    helper(mark_scale_channel,M,T,C).\n\n% @helper(mark_channel_field) Whether a mark has an encoding for a field and a channel.\nhelper(mark_channel_field,M,C,F) :-\n    entity(encoding,M,E),\n    attribute((encoding,channel),E,C),\n    helper((encoding,field),E,F).\n\n% @helper(mark_channel_discrete_or_binned) Whether a mark has a discrete scale or binned encoding for a channel.\nhelper(mark_channel_discrete_or_binned,M,C) :-\n    entity(encoding,M,E),\n    attribute((encoding,channel),E,C),\n    helper(mark_scale_channel,M,T,C),\n    domain(discrete_scale,T).\nhelper(mark_channel_discrete_or_binned,M,C) :-\n    entity(encoding,M,E),\n    attribute((encoding,channel),E,C),\n    attribute((encoding,binning),E,_).\n\n% @helper(mark_encoding_discrete_or_binned) Whether a mark has a discrete scale or binned encoding for an encoding.\nhelper(mark_encoding_discrete_or_binned,M,E) :-\n    entity(encoding,M,E),\n    attribute((encoding,channel),E,C),\n    helper(mark_scale_channel,M,T,C),\n    domain(discrete_scale,T).\nhelper(mark_encoding_discrete_or_binned,M,E) :-\n    entity(encoding,M,E),\n    attribute((encoding,binning),E,_).\n\n% @helper(mark_encoding_cont) Whether a mark has a continuous for an encoding.\nhelper(mark_encoding_cont,M,E) :-\n    entity(encoding,M,E),\n    not helper(mark_encoding_discrete_or_binned,M,E).\n\n% @helper(mark_channel_cont) Whether a mark has a continuous scale for a channel.\nhelper(mark_channel_cont,M,C) :-\n    helper((mark,channel),M,C),\n    not helper(mark_channel_discrete_or_binned,M,C).\n\n% @helper(encoding_cardinality) The cardinality of an encoding.\nhelper(encoding_cardinality,E,N) :-\n    attribute((field,unique),F,N),\n    helper((encoding,field),E,F),\n    not attribute((encoding,binning),E,_).\nhelper(encoding_cardinality,E,N) :-\n    attribute((encoding,binning),E,N).\n\n% @helper(discrete_cardinality) The cardinality of a discrete encoding.\nhelper(discrete_cardinality,M,E,N) :-\n    helper(encoding_cardinality,E,N),\n    helper(mark_encoding_discrete_or_binned,M,E).\n\n% @helper(facet_cardinality,Fa,N) The cardinality of a facet.\nhelper(facet_cardinality,Fa,N) :-\n    attribute((field,unique),Fi,N),\n    helper((facet,field),Fa,Fi),\n    not attribute((facet,binning),Fa,_).\nhelper(facet_cardinality,Fa,N) :-\n    attribute((facet,binning),Fa,N).\n\n% @helper(is_c_c) Continuous by continuous.\nhelper(is_c_c,M) :-\n    entity(mark,_,M),\n    helper((mark,channel),M,x),\n    helper((mark,channel),M,y),\n    not helper(mark_channel_discrete_or_binned,M,x),\n    not helper(mark_channel_discrete_or_binned,M,y).\n\n% @helper(is_c_d) Continuous by discrete (or only one continuous).\nhelper(is_c_d,M) :-\n    entity(mark,_,M),\n    helper(mark_channel_cont,M,x),\n    not helper(mark_channel_cont,M,y).\nhelper(is_c_d,M) :-\n    entity(mark,_,M),\n    not helper(mark_channel_cont,M,x),\n    helper(mark_channel_cont,M,y).\n\n% @helper(is_d_d) Discrete by discrete.\nhelper(is_d_d,M) :-\n    helper((mark,channel),M,x),\n    helper((mark,channel),M,y),\n    helper(mark_channel_discrete_or_binned,M,x),\n    helper(mark_channel_discrete_or_binned,M,y).\nhelper(is_d_d,M) :-\n    helper((mark,channel),M,x),\n    helper(mark_channel_discrete_or_binned,M,x),\n    not helper((mark,channel),M,y).\nhelper(is_d_d,M) :-\n    helper((mark,channel),M,y),\n    helper(mark_channel_discrete_or_binned,M,y),\n    not helper((mark,channel),M,x).\n\n% @helper(non_pos_unaggregated) Non-positional channels are unaggregated.\nhelper(non_pos_unaggregated,M) :-\n    entity(encoding,M,E),\n    attribute((encoding,channel),E,C),\n    domain(non_positional,C),\n    not attribute((encoding,aggregate),E,_).\n\n% @helper(no_overlap) The continuous variable is a measure (it is aggregated) and all other non-positional channels are aggregated, or we use stack.\nhelper(no_overlap,M) :-\n    helper(is_c_d,M),\n    helper(mark_encoding_cont,M,E),\n    attribute((encoding,channel),E,(x;y)),\n    attribute((encoding,aggregate),E,_),\n    not helper(non_pos_unaggregated,M).\nhelper(no_overlap,M) :-\n    helper(is_c_d,M),\n    entity(encoding,M,E),\n    attribute((encoding,stack),E,_).\nhelper(no_overlap,M) :-\n    helper(is_c_d,M),\n    attribute(number_rows,root,N),\n    helper(discrete_size,M,N).\nhelper(no_overlap,M) :-\n    helper(is_d_d,M),\n    helper((mark,channel),M,C),\n    domain(non_positional,C),\n    not helper(non_pos_unaggregated,M).\nhelper(no_overlap,M) :-\n    helper(is_d_d,M),\n    attribute(number_rows,root,N1),\n    helper(discrete_size,M,N2),\n    N1 <= N2.\n\n% @helper(overlap) We definitely overlap if the data size > discrete size.\nhelper(overlap,M) :-\n    helper(is_c_d,M),\n    not helper(no_overlap,M),\n    attribute(number_rows,root,N1),\n    helper(discrete_size,M,N2),\n    N1 > N2.\nhelper(overlap,M) :-\n    entity(mark,V,M),\n    helper(is_d_d,M),\n    not helper(no_overlap,M),\n    not entity(facet,V,_),\n    attribute(number_rows,root,N1),\n    helper(discrete_size,M,N2),\n    N1 > N2.\n\n% @helper(discrete_size) The size of the discrete positional encoding.\nhelper(discrete_size,M,N) :-\n    helper(is_c_d,M),\n    helper(x_cardinality,M,N).\nhelper(discrete_size,M,N) :-\n    helper(is_c_d,M),\n    helper(y_cardinality,M,N).\nhelper(discrete_size,M,1) :-\n    helper(is_c_d,M),\n    helper(mark_encoding_cont,M,E),\n    attribute((encoding,channel),E,x),\n    not helper((mark,channel),M,y).\nhelper(discrete_size,M,1) :-\n    helper(is_c_d,M),\n    helper(mark_encoding_cont,M,E),\n    attribute((encoding,channel),E,y),\n    not helper((mark,channel),M,x).\nhelper(discrete_size,M,N) :-\n    helper(is_d_d,M),\n    helper(x_cardinality,M,NX),\n    helper(y_cardinality,M,NY),\n    N = NX*NY.\n\n% @helper(x_cardinality) Cardinality of discrete x. Helps to go from quadratic to linear number of grounding.\nhelper(x_cardinality,M,N) :-\n    entity(encoding,M,E),\n    attribute((encoding,channel),E,x),\n    helper(discrete_cardinality,M,E,N).\nhelper(x_cardinality,M,1) :-\n    entity(mark,_,M),\n    not helper((mark,channel),M,x).\n\n% @helper(y_cardinality) Cardinality of discrete x. Helps to go from quadratic to linear number of grounding.\nhelper(y_cardinality,M,N) :-\n    entity(encoding,M,E),\n    attribute((encoding,channel),E,y),\n    helper(discrete_cardinality,M,E,N).\nhelper(y_cardinality,M,1) :-\n    entity(mark,_,M),\n    not helper((mark,channel),M,y).\n\n% @helper(enc_interesting) The field relevant to the task is mapped to the encoding E.\nhelper(enc_interesting,E) :-\n    helper((encoding,field),E,F),\n    attribute((field,interesting),F,true).\nhelper(enc_interesting,E) :-\n    helper((facet,field),E,F), % same field name here\n    attribute((field,interesting),F,true).\n\n% @helper(encoding_field) The encoding field name matches the id.\nhelper((encoding,field),E,F) :-\n    attribute((encoding,field),E,N),\n    attribute((field,name),F,N).\n\n% @helper(facet_field) The encoding field name matches the id.\nhelper((facet,field),FC,F) :-\n    attribute((facet,field),FC,N),\n    attribute((field,name),F,N).\n\n% @helper(field_skewed) Whether a field is skewed (absolute skew > 5).\nhelper(field_skewed,F) :-\n    attribute((field,skew),F,S),\n    |S| > 2.\n\n% @helper(total_facet_cardinality) The total cardinality of all facets in a view (product of individual facet cardinalities).\nhelper(total_facet_cardinality,V,1) :-\n    entity(view,root,V),\n    not entity(facet,V,_).\nhelper(total_facet_cardinality,V,N) :-\n    entity(facet,V,F),\n    attribute((facet,channel),F,row),\n    helper(facet_cardinality,F,N),\n    not entity(facet,V,F2),\n    F2 != F,\n    attribute((facet,channel),F2,col).\nhelper(total_facet_cardinality,V,N) :-\n    entity(facet,V,F),\n    attribute((facet,channel),F,col),\n    helper(facet_cardinality,F,N),\n    not entity(facet,V,F2),\n    F2 != F,\n    attribute((facet,channel),F2,row).\nhelper(total_facet_cardinality,V,N) :-\n    entity(facet,V,F1),\n    entity(facet,V,F2),\n    F1 != F2,\n    attribute((facet,channel),F1,row),\n    attribute((facet,channel),F2,col),\n    helper(facet_cardinality,F1,N1),\n    helper(facet_cardinality,F2,N2),\n    N = N1 * N2.\n"
      },
      {
        "name": "generate",
        "src": "% ====== Generators ======\n\n% helpers to generate attributes based on whether they are required.\n{ attribute(N,root,V) : domain(N,V) } = 1 :- root_required(N).\n{ attribute((N,A),E,V): domain((N,A),V) } = 1 :- entity(N,_,E), required((N,A)).\n0 { attribute((N,A),E,V): domain((N,A),V) } 1 :- entity(N,_,E), not_required((N,A)).\n\n% maximum number of non-layered views.\n#const max_views = 1.\n{ entity(view,root,(v,0..max_views-1)) }.\n:- { entity(view,root,_) } > max_views.\n:- entity(view,root,(v,ID)), not entity(view,root,(v,ID-1)), ID > 0.\n\n% @generator(coordinates) Each view requires coordinates.\nrequired((view,coordinates)).\n\n% maximum number of additional marks for each view.\n#const max_marks = 2.\n{ mark_id(V,0..max_marks-1) } :- entity(view,root,V).\n:- mark_id(V,ID), not mark_id(V,ID-1), ID > 0.\nentity(mark,V,(V,M)) :- mark_id(V,M).\n\n% maximum number for additional encoding channels.\n#const max_encs = 4.\n{ enc_id(M,0..max_encs-1) } :- entity(mark,V,M).\n:- enc_id(M,ID), not enc_id(M,ID-1), ID > 0.\nentity(encoding,M,(M,E)) :- enc_id(M,E).\n\n% @generate(encoding_channel) Each encoding requires a channel.\nrequired((encoding,channel)).\n% @generator(mark_type) Each mark requires a type.\nrequired((mark,type)).\n% @generator(task) Each root requires a task.\nroot_required(task).\n\n% @generator(encoding_attribute) Encoding with binning, aggregate, field or stack.\nnot_required((encoding,binning)).\nnot_required((encoding,aggregate)).\nnot_required((encoding,stack)).\n0 { attribute((encoding,field),E,N): domain((field,name),N) } 1 :- entity(encoding,_,E).\n\n% generator(scale) generate scales such that their ids and channels corresponds to the encodings in each view.\n{ entity(scale,V,(s,E)) } :- entity(mark,V,M), entity(encoding,M,E), attribute((encoding,channel),E,C).\nattribute((scale,channel),(s,E),C) :- entity(scale,V,(s,E)), attribute((encoding,channel),E,C).\n\nrequired((scale,channel)).\nrequired((scale,type)).\nnot_required((scale,zero)).\n\n% generator(facet) Each specification can have at most both row and col facet.\nfacet_id(0..1).\n{ entity(facet,V,(fc,F)) : facet_id(F), entity(view,root,V) }.\n:- entity(facet,V,(fc,1)), not entity(facet,V,(fc,0)),facet_id(1), facet_id(0).\n\nrequired((facet,channel)).\nnot_required((facet,binning)).\n{ attribute((facet,field),E,N): domain((field,name),N) } = 1 :- entity(facet,_,E).\n"
      },
      {
        "name": "hard",
        "src": "% ====== Hard constraints ======\n\n% @hard(scale_type_data_type) Primitive type has to support scale type.\nviolation(scale_type_data_type) :-\n    attribute((field,type),F,(string;boolean)),\n    helper((encoding,field),E,F),\n    helper((encoding,scale_type),E,T),\n    domain(continuous_scale,T).\n\n% @hard(log_non_positive) Cannot use log if the data is negative or zero.\nviolation(log_non_positive) :-\n    attribute((field,min),F,MIN),\n    helper((encoding,field),E,F),\n    helper((encoding,scale_type),E,log),\n    MIN <= 0.\n\n% @hard(log_zero_included) Cannot use the log scale if the extent includes zero.\nviolation(log_zero_included) :-\n    attribute((field,min),F,MIN),\n    attribute((field,max),F,MAX),\n    MIN * MAX <= 0,\n    entity(encoding,M,E),\n    helper((encoding,field),E,F),\n    helper(mark_encoding_scale,M,E,S),\n    attribute((scale,type),S,log).\n\n% @hard(bin_and_aggregate) Cannot bin and aggregate.\nviolation(bin_and_aggregate) :-\n    attribute((encoding,binning),E,_),\n    attribute((encoding,aggregate),E,_).\n\n% @hard(aggregate_t_valid) Temporal scale only supports min and max.\nviolation(aggregate_t_valid) :-\n    attribute((field,type),F,datetime),\n    helper((encoding,field),E,F),\n    attribute((encoding,aggregate),E,A),\n    A != min,\n    A != max.\n\n% @hard(aggregate_num_valid) Only numbers can be aggregated with mean, sum, stdev\nviolation(aggregate_num_valid) :-\n    attribute((field,type),F,T),\n    helper((encoding,field),E,F),\n    attribute((encoding,aggregate),E,(mean;sum;stdev)),\n    T != number.\n\n% @hard(bin_n_d) Only numbers and datetimes can be binned\nviolation(bin_n_d) :-\n    attribute((field,type),F,T),\n    helper((encoding,field),E,F),\n    attribute((encoding,binning),E,_),\n    T != number,\n    T != datetime.\n\n% @hard(aggregate_detail) Detail cannot be aggregated.\nviolation(aggregate_detail) :-\n    attribute((encoding,channel),E,detail),\n    attribute((encoding,aggregate),E,_).\n\n% @hard(count_without_q) Count has to have a continuous scale.\nviolation(count_without_q) :-\n    attribute((encoding,aggregate),E,count),\n    helper((encoding,scale_type),E,T),\n    domain(discrete_scale,T).\n\n% @hard(shape_not_ordinal) Shape requires discrete and ordinal (ordinal/categorical doesn't matter as scale types, so we use ordinal only here).\nviolation(shape_not_ordinal) :-\n    helper(mark_scale_channel,_,T,shape),\n    T != ordinal.\n\n% @hard(categorical_not_color) Categorical only works with color channel.\nviolation(categorical_not_color) :-\n    attribute((scale,type),S,categorical),\n    not attribute((scale,channel),S,color).\n\n% @hard(size_negative) Do not use size when data is negative as size implies that data is positive.\nviolation(size_negative) :-\n    attribute((encoding,channel),E,size),\n    helper((encoding,field),E,F),\n    attribute((field,min),F,MIN),\n    attribute((field,max),F,MAX),\n    MIN < 0,\n    MAX > 0.\n\n% @hard(encoding_repeat_channel) Cannot use single channels twice for the same mark.\nviolation(encoding_repeat_channel) :-\n    entity(mark,_,M),\n    domain(single_channel,C),\n    2 <= #count { E : entity(encoding,M,E), attribute((encoding,channel),E,C) }.\n\n% @hard(scale_repeat_channel) Cannot use single channels twice for the same view.\nviolation(scale_repeat_channel) :-\n    entity(view,root,V),\n    domain(single_channel,C),\n    2 <= #count { S : entity(scale,V,S), attribute((scale,channel),S,C) }.\n\n% @hard(encoding_channel_without_scale) Encoding channel doesn't have a corresponding scale channel.\nviolation(encoding_channel_without_scale) :-\n    entity(mark,V,M),\n    helper((mark,channel),M,C),\n    not domain(scale_channel,V,C).\n\n% @hard(scale_channel_without_encoding) Scale channel doesn't have a corresponding encoding channel.\nviolation(scale_channel_without_encoding) :-\n    entity(view,_,V),\n    entity(scale,V,S),\n    attribute((scale,channel),S,C),\n    not domain(encoding_channel,V,C).\n\n% @hard(no_encodings) There has to be at least one encoding for every mark.\nviolation(no_encodings) :-\n    entity(mark,_,M),\n    not entity(encoding,M,_).\n\n% @hard(encoding_no_field_and_not_count) All encodings (if they have a channel) require field except if we have a count aggregate.\nviolation(encoding_no_field_and_not_count) :-\n    entity(encoding,_,E),\n    not attribute((encoding,field),E,_),\n    not attribute((encoding,aggregate),E,count).\n\n% @hard(count_with_field) Count should not have a field. Having a field doesn't make a difference.\nviolation(count_with_field) :-\n    attribute((encoding,aggregate),E,count),\n    helper((encoding,field),E,_).\n\n% @hard(text_mark_without_text_channel) Text mark requires text encoding.\nviolation(text_mark_without_text_channel) :-\n    attribute((mark,type),M,text),\n    not helper((mark,channel),M,text).\n\n% @hard(text_channel_without_text_mark) Text channel requires text mark.\nviolation(text_channel_without_text_mark) :-\n    helper((mark,channel),M,text),\n    not attribute((mark,type),M,text).\n\n% @hard(point_tick_bar_without_x_or_y) Point, tick, and bar require x or y channel.\nviolation(point_tick_bar_without_x_or_y) :-\n    attribute((mark,type),M,(point;tick;bar)),\n    not helper((mark,channel),M,x),\n    not helper((mark,channel),M,y).\n\n% @hard(line_area_without_x_y) Line and area require x and y channel.\nviolation(line_area_without_x_y) :-\n    attribute((mark,type),M,(line;area)),\n    { helper((mark,channel),M,x);helper((mark,channel),M,y) } <= 1.\n\n% @hard(line_area_with_discrete) Line and area cannot have both x and y discrete.\nviolation(line_area_with_discrete) :-\n    attribute((mark,type),M,(line;area)),\n    helper(mark_scale_channel,M,T1,x),\n    helper(mark_scale_channel,M,T2,y),\n    domain(discrete_scale,T1),\n    domain(discrete_scale,T2).\n\n% @hard(bar_tick_continuous_x_y) Bar and tick cannot have both x and y continuous.\nviolation(bar_tick_continuous_x_y) :-\n    attribute((mark,type),M,(tick;bar)),\n    helper(is_c_c,M).\n\n% @hard(view_scale_conflict) A view cannot have a scale definition that conflicts with a shared scale for the same channel.\nviolation(view_scale_conflict) :-\n    entity(view,R,V),\n    entity(scale,R,S1),\n    entity(scale,V,S2),\n    attribute((scale,channel),S1,C),\n    attribute((scale,channel),S2,C).\n\n% @hard(shape_without_point) Shape channel requires point mark.\nviolation(shape_without_point) :-\n    helper((mark,channel),M,shape),\n    not attribute((mark,type),M,point).\n\n% @hard(size_without_point_text) Size only works with some marks.\nviolation(size_without_point_text) :-\n    helper((mark,channel),M,size),\n    not attribute((mark,type),M,text),\n    not attribute((mark,type),M,point).\n\n% @hard(detail_without_agg) Detail requires aggregation. Detail adds a field to the group by. Detail could also be used to add information to tooltips. We may remove this later.\nviolation(detail_without_agg) :-\n    entity(encoding,M,E1),\n    entity(encoding,M,E2),\n    E1 != E2,\n    attribute((encoding,channel),E1,detail),\n    not attribute((encoding,aggregate),E2,_).\n\n% @hard(area_bar_with_log) Do not use log for bar or area mark as they are often misleading. We may remove this rule in the future.\nviolation(area_bar_with_log) :-\n    attribute((mark,type),M,(bar;area)),\n    helper(mark_scale_channel,M,log,(x;y)).\n\n% @hard(rect_without_d_d) Rect mark needs discrete x and y.\nviolation(rect_without_d_d) :-\n    attribute((mark,type),M,rect),\n    helper(mark_scale_channel,M,T,(x;y)),\n    domain(continuous_scale,T).\n\n% @hard(same_field_x_and_y) Don't use the same field on x and y.\nviolation(same_field_x_and_y) :-\n    helper(mark_channel_field,M,x,F),\n    helper(mark_channel_field,M,y,F),\n    entity(field,root,F),\n    entity(mark,_,M).\n\n% @hard(count_twice) Don't use count twice.\nviolation(count_twice):-\n    { entity(encoding,M,E) : attribute((encoding,aggregate),E,count) } >= 2,\n    entity(mark,_,M).\n\n% @hard(aggregate_not_all_continuous) If we use aggregation, then all continuous scales need to be aggregated.\nviolation(aggregate_not_all_continuous):-\n    attribute((encoding,aggregate),E1,_),\n    entity(encoding,M,E1),\n    entity(encoding,M,E2),\n    E1 > E2,\n    helper((encoding,scale_type),E2,T),\n    domain(continuous_scale,T),\n    not attribute((encoding,binning),E2,_),\n    not attribute((encoding,aggregate),E2,_).\n\n% @hard(detail_not_ordinal) Detail requires ordinal scales.\nviolation(detail_not_ordinal) :-\n    attribute((scale,channel),S,detail),\n    not attribute((scale,type),S,ordinal).\n\n% @hard(bar_tick_area_line_without_continuous_x_y) Bar, tick, line, area require some continuous variable on x or y.\nviolation(bar_tick_area_line_without_continuous_x_y) :-\n    attribute((mark,type),M,(bar;tick;area;line)),\n    { helper(mark_channel_cont,M,x);helper(mark_channel_cont,M,y) } <= 0.\n\n% @hard(zero_d_n) Can only use zero with datetime or number.\nviolation(zero_d_n) :-\n    helper((mark,scale),M,S),\n    entity(encoding,M,E),\n    attribute((encoding,channel),E,C),\n    attribute((scale,zero),S,_),\n    attribute((scale,channel),S,C),\n    helper((encoding,field),E,F),\n    attribute((field,type),F,T),\n    T != datetime,\n    T != number.\n\n% @hard(zero_linear) Can only use zero with linear scale.\nviolation(zero_linear) :-\n    entity(scale,_,S),\n    not attribute((scale,type),S,linear),\n    attribute((scale,zero),S,true).\n\n% @hard(bar_area_without_zero) Bar and area mark requires scale of continuous to start at zero.\nviolation(bar_area_without_zero) :-\n    attribute((mark,type),M,(bar;area)),\n    entity(encoding,M,E),\n    attribute((encoding,channel),E,C),\n    not attribute((encoding,binning),E,_),\n    helper((mark,scale),M,S),\n    attribute((scale,channel),S,C),\n    attribute((scale,type),S,T),\n    domain(continuous_scale,T),\n    not attribute((scale,zero),S,_),\n    C = (x;y).\n\n% @hard(row_no_y) Don't use row without y. Just using y is simpler.\nviolation(row_no_y) :-\n    entity(facet,V,F),\n    attribute((facet,channel),F,row),\n    entity(mark,V,M),\n    not helper((mark,channel),M,y).\n\n% @hard(col_no_x) Don't use column without x. Just using x is simpler.\nviolation(col_no_x) :-\n    entity(facet,V,F),\n    attribute((facet,channel),F,col),\n    entity(mark,V,M),\n    not helper((mark,channel),M,x).\n\n% @hard(facet_no_duplicate_field) Don't use the same field twice when faceting.\nviolation(facet_no_duplicate_field) :-\n    entity(facet,V,F1),\n    entity(facet,V,F2),\n    F1 != F2,\n    attribute((facet,field),F1,F),\n    attribute((facet,field),F2,F).\n\n% @hard(facet_no_duplicate_channel_on_same_view) Don't use the same channel twice for faceting on the same view.\nviolation(facet_no_duplicate_channel_on_same_view) :-\n    entity(view,root,V),\n    entity(facet,V,F1),\n    entity(facet,V,F2),\n    F1 != F2,\n    attribute((facet,channel),F1,C),\n    attribute((facet,channel),F2,C).\n\n% @hard(stack_without_bar_area) Only use stacking for bar and area.\nviolation(stack_without_bar_area) :-\n    helper(mark_with_stack,M),\n    not attribute((mark,type),M,bar),\n    not attribute((mark,type),M,area).\n\n% @hard(stack_without_summative_agg) Don't stack if aggregation is not summative (summative are count, sum, distinct, valid, missing).\nviolation(stack_without_summative_agg) :-\n    entity(encoding,_,E),\n    attribute((encoding,stack),E,_),\n    not attribute((encoding,aggregate),E,sum),\n    not attribute((encoding,aggregate),E,count).\n\n% @hard(no_stack_with_bar_area_discrete_color) Need to stack if we use bar, area with discrete color.\nviolation(no_stack_with_bar_area_discrete_color) :-\n    helper(mark_channel_discrete_or_binned,M,color),\n    attribute((mark,type),M,(bar;area)),\n    not helper(mark_with_stack,M).\n\n% @hard(stack_without_discrete_color_or_detail) Can only use stack if we also use discrete color, or detail.\nviolation(stack_without_discrete_color_or_detail) :-\n    helper(mark_with_stack,M),\n    not helper(mark_channel_discrete_or_binned,M,color),\n    not helper((mark,channel),M,detail).\n\n% @hard(stack_without_x_y) Stack can only be on x or y.\nviolation(stack_without_x_y) :-\n    attribute((encoding,stack),E,_),\n    not attribute((encoding,channel),E,x),\n    not attribute((encoding,channel),E,y).\n\n% @hard(stack_discrete) Stack can only be on continuous.\nviolation(stack_discrete) :-\n    attribute((encoding,channel),E,C),\n    attribute((encoding,stack),E,_),\n    helper(mark_channel_discrete_or_binned,_,C).\n\n% @hard(stack_with_non_positional_non_agg) Cannot use non positional continuous with stack unless it's aggregated.\nviolation(stack_with_non_positional_non_agg) :-\n    helper(mark_with_stack,M),\n    entity(encoding,M,E),\n    attribute((encoding,channel),E,C),\n    domain(non_positional,C),\n    not attribute((encoding,aggregate),E,_),\n    not helper(mark_channel_discrete_or_binned,M,C).\n\n% @hard(invalid_bin) Check bin type.\nviolation(invalid_bin) :-\n    attribute((encoding,binning),_,B),\n    B < 0.\n\n% @hard(invalid_num_rows) number_rows has to be larger than 0.\nviolation(invalid_num_rows) :-\n    attribute(number_rows,root,R),\n    R <= 0.\n\n% @hard(invalid_unique) The number of unique values has to be larger than 0.\nviolation(invalid_unique) :-\n    attribute((field,unique),_,U),\n    U <= 0.\n\n% @hard(invalid_extent_non_number) Extent only allowed for numbers (for now).\nviolation(invalid_extent_non_number) :-\n    attribute((field,(min;max)),F,_),\n    not attribute((field,type),F,number).\n\n% @hard(invalid_non_number_std) Std only allowed for numbers (for now).\nviolation(invalid_non_number_std) :-\n    attribute((field,std),F,_),\n    not attribute((field,type),F,number).\n\n% @hard(invalid_std) Std has to be larger or equal to 0.\nviolation(invalid_std) :-\n    attribute((field,std),_,S),\n    S < 0.\n\n% @hard(invalid_extent_order) Order has to be correct.\nviolation(invalid_extent_order) :-\n    attribute((field,min),F,MIN),\n    attribute((field,max),F,MAX),\n    MIN > MAX.\n\n% @hard(invalid_non_string_freq) Frequency for strings only.\nviolation(invalid_non_string_freq) :-\n    attribute((field,freq),F,_),\n    not attribute((field,type),F,string).\n\n% @hard(enforce_order) property should follow natural order for generated entities.\nviolation(enforce_order):-\n    entity(view,root,V), M1 < M2,\n    attribute((mark,type),(V,M1),T1),\n    attribute((mark,type),(V,M2),T2),\n    not T1 < T2.\nviolation(enforce_order):-\n    entity(mark,_,M), E1 < E2,\n    attribute((encoding,channel),(M,E1),C1),\n    attribute((encoding,channel),(M,E2),C2),\n    not C1 < C2.\n"
      },
      {
        "name": "soft",
        "src": "% ====== Preferences ======\n\n% @soft(aggregate) Prefer to use raw (no aggregate).\npreference(aggregate,E) :-\n    attribute((encoding,aggregate),E,_).\n\n% @soft(bin) Prefer to not bin.\npreference(bin,E) :-\n    attribute((encoding,binning),E,_).\npreference(bin,Fa) :-\n    attribute((facet,binning),Fa,_).\n\n% @soft(bin_high) Prefer binning with at most 12 buckets.\npreference(bin_high,E) :-\n    attribute((encoding,binning),E,B), B > 12.\n\n% @soft(bin_low) Prefer binning with more than 7 buckets.\npreference(bin_low,E) :-\n    attribute((encoding,binning),E,B), B <= 7.\n\n% @soft(encoding) Prefer to use fewer encodings.\npreference(encoding,E) :-\n    entity(encoding,_,E).\n\n% @soft(encoding_field) Prefer to use fewer encodings with fields (count does not have a field).\npreference(encoding_field,E) :-\n    attribute((encoding,field),E,_).\n\n% @soft(facet_field) Prefer to use fewer facets with fields.\npreference(facet_field,F) :-\n    attribute((facet,field),F,_).\n\n% @soft(facet_used_before_pos) Prefer not to use faceting until both x and y channels are used.\npreference(facet_used_before_pos,V) :-\n    entity(facet,V,_),\n    entity(mark,V,M),\n    not helper((mark,channel),M,x).\npreference(facet_used_before_pos,V) :-\n    entity(facet,V,_),\n    entity(mark,V,M),\n    not helper((mark,channel),M,y).\n\n% @soft(facet_used_before_color) Prefer not to use faceting until color channel is used.\npreference(facet_used_before_color,V) :-\n    entity(facet,V,_),\n    entity(mark,V,M),\n    not helper((mark,channel),M,color).\n\n% @soft(facet_row) Prefer to use column facets over row facets.\npreference(facet_row,F) :-\n    attribute((facet,channel),F,row).\n\n% @soft(facet_col) Prefer to use row facets over column facets.\npreference(facet_col,F) :-\n    attribute((facet,channel),F,col).\n\n% @soft(same_field) Prefer not to use the same field twice for the same mark.\npreference(same_field,F) :-\n    entity(field,_,F),\n    entity(mark,_,M),\n    { entity(encoding,M,E): helper((encoding,field),E,F) } = 2.\n\n% @soft(same_field_grt3) Prefer not to use the same field three or more times for the same mark.\npreference(same_field_grt3,F) :-\n    entity(field,_,F),\n    entity(mark,_,M),\n    { entity(encoding,M,E): helper((encoding,field),E,F) } >= 3.\n\n% @soft(same_mark_type) Prefer not to use the same mark type more than once in the same view.\npreference(same_mark_type,V) :-\n    entity(mark,V,M1),\n    entity(mark,V,M2),\n    M1 != M2,\n    attribute((mark,type),M1,Type),\n    attribute((mark,type),M2,Type).\n\n% @soft(same_channel) Prefer not to use the same channel more than once in the same view.\npreference(same_channel,V) :-\n    entity(encoding,M1,E1),\n    entity(encoding,M2,E2),\n    entity(mark,V,M1),\n    entity(mark,V,M2),\n    E1 != E2,\n    attribute((encoding,channel),E1,Channel),\n    attribute((encoding,channel),E2,Channel).\n\n% @soft(count_grt1) Prefer not to use count more than once.\npreference(count_grt1,M) :-\n    entity(mark,_,M),\n    { entity(encoding,M,E): attribute((encoding,aggregate),E,count) } > 1.\n\n% @soft(number_categorical) Should not use categorical scale for number field.\npreference(number_categorical,E) :-\n    attribute((field,type),F,number),\n    helper((encoding,field),E,F),\n    helper((encoding,scale_type),E,categorical).\n\n% @soft(bin_low_unique) Binned field should not have too less unique values.\npreference(bin_low_unique,E) :-\n    attribute((field,type),F,(number;datetime)),\n    attribute((field,unique),F,U),\n    helper((encoding,field),E,F),\n    attribute((encoding,binning),E,_),\n    U < 40.\n\n% @soft(bin_not_linear) Prefer linear scale for bin.\npreference(bin_not_linear,E) :-\n    attribute((encoding,binning),E,_),\n    not helper((encoding,scale_type),E,linear).\n\n% @soft(bin_string) Prefer not to bin string fields.\npreference(bin_string,E) :-\n    attribute((field,type),F,string),\n    helper((encoding,field),E,F),\n    attribute((encoding,binning),E,_).\npreference(bin_string,Fa) :-\n    attribute((field,type),F,string),\n    helper((facet,field),Fa,F),\n    attribute((facet,binning),Fa,_).\n\n% @soft(only_discrete) Only discrete encoding channels are used in a mark.\npreference(only_discrete,M) :-\n    entity(mark,_,M),\n    not helper(mark_encoding_cont,M,_).\n\n% @soft(multi_non_pos) Prefer not to use multiple non-positional encoding channels.\npreference(multi_non_pos,M) :-\n    entity(mark,_,M),\n    { helper((mark,channel),M,C): domain(non_positional,C) } > 1.\n\n% @soft(non_pos_used_before_pos) Prefer not to use non-positional channels until all positional channels are used.\npreference(non_pos_used_before_pos,M) :-\n    helper((mark,channel),M,C),\n    domain(non_positional,C),\n    not helper((mark,channel),M,(x;y)).\n\n% @soft(aggregate_group_by_raw) Aggregate plots should not use raw continuous as group by.\npreference(aggregate_group_by_raw,E) :-\n    entity(encoding,M,EA),\n    attribute((encoding,aggregate),EA,_),\n    entity(encoding,M,E),\n    not helper(mark_encoding_discrete_or_binned,M,E),\n    not attribute((encoding,aggregate),E,_).\n\n% @soft(aggregate_no_discrete) Aggregate should also have a discrete encoding to group by.\npreference(aggregate_no_discrete,M) :-\n    entity(encoding,M,EA),\n    attribute((encoding,aggregate),EA,_),\n    not helper(mark_encoding_discrete_or_binned,M,_).\n\n% @soft(x_y_raw) Prefer not to use plot with both x and y discrete and no aggregate as it leads to occlusion.\npreference(x_y_raw,M) :-\n    helper(mark_channel_discrete_or_binned,M,x),\n    helper(mark_channel_discrete_or_binned,M,y),\n    entity(encoding,M,E),\n    not helper(mark_encoding_discrete_or_binned,M,E),\n    not attribute((encoding,aggregate),E,_).\n\n% @soft(continuous_not_zero) Prefer to include zero for continuous (binned doesn't need zero).\npreference(continuous_not_zero,E) :-\n    not helper(mark_encoding_discrete_or_binned,M,E),\n    helper(mark_encoding_scale,M,E,S),\n    not attribute((scale,zero),S,true).\n\n% @soft(size_not_zero) Prefer zero size (even when binned).\npreference(size_not_zero,E) :-\n    attribute((encoding,channel),E,size),\n    helper(mark_encoding_scale,M,E,S),\n    not attribute((scale,zero),S,true).\n\n% @soft(continuous_pos_not_zero) Prefer zero continuous positional.\npreference(continuous_pos_not_zero,E) :-\n    attribute((encoding,channel),E,(x;y)),\n    not helper(mark_encoding_discrete_or_binned,M,E),\n    helper(mark_encoding_scale,M,E,S),\n    not attribute((scale,zero),S,true).\n\n% @soft(skew_zero) Prefer not to use zero when the difference between min and max is smaller than distance to 0.\npreference(skew_zero,E) :-\n    attribute((field,min),F,MIN),\n    attribute((field,max),F,MAX),\n    EXT = MAX - MIN,\n    |MAX| > EXT,\n    |MIN| > EXT,\n    entity(encoding,M,E),\n    helper((encoding,field),E,F),\n    helper(mark_encoding_scale,M,E,S),\n    attribute((scale,zero),S,true).\n\n% @soft(cross_zero) Prefer not to include zero as baseline when the range of data crosses zero.\npreference(cross_zero,E) :-\n    attribute((field,min),F,MIN),\n    attribute((field,max),F,MAX),\n    MAX > 0,\n    MIN < 0,\n    entity(encoding,M,E),\n    helper((encoding,field),E,F),\n    helper(mark_encoding_scale,M,E,S),\n    attribute((scale,zero),S,true).\n\n% @soft(only_y) Prefer to use only x instead of only y.\npreference(only_y,M) :-\n    helper((mark,channel),M,y),\n    not helper((mark,channel),M,x).\n\n% @soft(binned_orientation_not_x) Prefer binned quantitative on x axis.\npreference(binned_orientation_not_x,E) :-\n    attribute((field,type),F,(number;datetime)),\n    helper((encoding,field),E,F),\n    attribute((encoding,binning),E,_),\n    not attribute((encoding,channel),_,x).\n\n% @soft(high_cardinality_ordinal) Prefer not to use ordinal for fields with high cardinality.\npreference(high_cardinality_ordinal,E) :-\n    helper(encoding_cardinality,E,N),\n    helper((encoding,scale_type),E,ordinal),\n    N > 30.\n\n% @soft(high_cardinality_categorical_grt10) Prefer not to use categorical (color) for fields with high cardinality.\npreference(high_cardinality_categorical_grt10,E) :-\n    helper(encoding_cardinality,E,N),\n    helper((encoding,scale_type),E,categorical),\n    N > 10.\n\n% @soft(high_cardinality_categorical_line_point_color) Prefer faceting over color for line/point marks with high cardinality categorical data to avoid overplotting.\npreference(high_cardinality_categorical_line_point_color,E) :-\n    entity(encoding,M,E),\n    attribute((mark,type),M,(line;point)),\n    attribute((encoding,channel),E,color),\n    helper((encoding,field),E,F),\n    helper((encoding,scale_type),E,categorical),\n    helper(encoding_cardinality,E,N),\n    N > 4.\n\n% @soft(high_cardinality_point_color_before_facet) Prefer faceting on both row and col before color for point marks with high cardinality to avoid overplotting.\npreference(high_cardinality_point_color_before_facet,V) :-\n    entity(mark,V,M),\n    attribute((mark,type),M,point),\n    entity(encoding,M,E),\n    helper(encoding_cardinality,E,N),\n    N > 75,\n    helper((mark,channel),M,color),\n    not entity(facet,V,_).\npreference(high_cardinality_point_color_before_facet,V) :-\n    entity(mark,V,M),\n    attribute((mark,type),M,point),\n    entity(encoding,M,E),\n    helper(encoding_cardinality,E,N),\n    N > 75,\n    helper((mark,channel),M,color),\n    entity(facet,V,F1),\n    attribute((facet,channel),F1,row),\n    not entity(facet,V,F2) : attribute((facet,channel),F2,col).\npreference(high_cardinality_point_color_before_facet,V) :-\n    entity(mark,V,M),\n    attribute((mark,type),M,point),\n    entity(encoding,M,E),\n    helper(encoding_cardinality,E,N),\n    N > 75,\n    helper((mark,channel),M,color),\n    entity(facet,V,F1),\n    attribute((facet,channel),F1,col),\n    not entity(facet,V,F2) : attribute((facet,channel),F2,row).\n\n% @soft(high_cardinality_categorical_ordinal_color) Prefer not to use ordinal scale with color for categorical data with high cardinality.\npreference(high_cardinality_categorical_ordinal_color,E) :-\n    attribute((encoding,channel),E,color),\n    helper((encoding,field),E,F),\n    attribute((field,type),F,string),\n    helper((encoding,scale_type),E,ordinal),\n    helper(encoding_cardinality,E,N),\n    N > 4.\n\n% @soft(high_cardinality_shape) Prefer not to use high cardinality ordinal for shape.\npreference(high_cardinality_shape,E) :-\n    helper(encoding_cardinality,E,N),\n    attribute((encoding,channel),E,shape),\n    N > 8.\n\n% @soft(high_cardinality_size) Prefer not to use size when the cardinality is large on x or y.\npreference(high_cardinality_size,E) :-\n    helper((mark,channel),M,size),\n    entity(encoding,M,E),\n    helper(encoding_cardinality,E,N),\n    attribute((encoding,channel),E,(x;y)),\n    not helper(mark_encoding_discrete_or_binned,M,E),\n    N > 100.\n\n% @soft(high_cardinality_facet) Prefer not to use faceting on row and col when field cardinality is large.\npreference(high_cardinality_facet,F) :-\n    attribute((facet,channel),F,(row;col)),\n    helper(facet_cardinality,F,N),\n    N > 12.\n\n% @soft(underplotting) Prefer not to facet when it results in too few data points per facet.\npreference(underplotting,V) :-\n    entity(facet,V,_),\n    attribute(number_rows,root,NR),\n    helper(total_facet_cardinality,V,NF),\n    NR < 3 * NF.\n\n% @soft(horizontal_scrolling_x) Avoid high cardinality on x as it causes horizontal scrolling.\npreference(horizontal_scrolling_x,E) :-\n    attribute((encoding,channel),E,x),\n    helper(encoding_cardinality,E,N),\n    helper(mark_encoding_discrete_or_binned,_,E),\n    N > 50.\n\n% @soft(horizontal_scrolling_col) Avoid high cardinality on column as it causes horizontal scrolling.\npreference(horizontal_scrolling_col,F) :-\n    attribute((facet,channel),F,col),\n    helper(facet_cardinality,F,N),\n    N > 5.\n\n% @soft(vertical_scrolling_y) Avoid high cardinality on y as it causes vertical scrolling.\npreference(vertical_scrolling_y,E) :-\n    attribute((encoding,channel),E,y),\n    helper(encoding_cardinality,E,N),\n    helper(mark_encoding_discrete_or_binned,_,E),\n    N > 50.\n\n% @soft(vertical_scrolling_row) Avoid high cardinality on row as it causes vertical scrolling.\npreference(vertical_scrolling_row,F) :-\n    attribute((facet,channel),F,row),\n    helper(facet_cardinality,F,N),\n    N > 5.\n\n% @soft(date_scale) Prefer to use linear/ordinal scale type with dates.\npreference(date_scale,E) :-\n    attribute((field,type),F,datetime),\n    helper((encoding,field),E,F),\n    not helper((encoding,scale_type),E,linear),\n    not helper((encoding,scale_type),E,ordinal).\n\n% @soft(number_linear) Prefer use linear for numbers with high cardinality.\npreference(number_linear,E) :-\n    attribute((field,type),F,number),\n    attribute((field,unique),F,N),\n    N > 20,\n    helper((encoding,field),E,F),\n    not attribute((encoding,binning),E,_),\n    not helper((encoding,scale_type),E,linear).\n\n% @soft(position_entropy) Overplotting. Prefer not to use x and y for continuous with high cardinality and low entropy without aggregation because the points will overplot.\npreference(position_entropy,E) :-\n    attribute((encoding,channel),E,(x;y)),\n    attribute(encoding_cardinality,E,N),\n    N > 100,\n    attribute((field,entropy),F,EN),\n    helper((encoding,field),E,F),\n    EN <= 3000,\n    not helper(mark_encoding_discrete_or_binned,_,E),\n    not attribute((encoding,aggregate),E,_).\n\n% @soft(value_agg) Prefer not to aggregate for value tasks.\npreference(value_agg,V) :-\n    attribute(task,root,value),\n    entity(mark,V,M),\n    entity(encoding,M,E),\n    attribute((encoding,aggregate),E,_).\n\n% @soft(value_bin) Prefer not to bin for value tasks as it reduces detail needed for value lookup.\npreference(value_bin,E) :-\n    attribute(task,root,value),\n    attribute((encoding,binning),E,_).\npreference(value_bin,Fa) :-\n    attribute(task,root,value),\n    attribute((facet,binning),Fa,_).\n\n% @soft(summary_facet) Prefer not to use facet for summary tasks as it makes it difficult to compare.\npreference(summary_facet,V) :-\n    attribute(task,root,summary),\n    entity(facet,V,_).\n\n% @soft(c_d_col) Prefer not to use continuous on x, discrete on y, and column.\npreference(c_d_col,V) :-\n    entity(mark,V,M),\n    not helper(mark_channel_discrete_or_binned,M,x),\n    helper(mark_channel_discrete_or_binned,M,y),\n    entity(facet,V,F),\n    attribute((facet,channel),F,col).\n\n% @soft(date_not_x) Prefer datetime on x.\npreference(date_not_x,E) :-\n    attribute((field,type),F,datetime),\n    helper((encoding,field),E,F),\n    not attribute((encoding,channel),E,x).\n\n% @soft(date_not_line) Prefer line mark when using datetime field.\npreference(date_not_line,M) :-\n    entity(encoding,M,E),\n    helper((encoding,field),E,F),\n    attribute((field,type),F,datetime),\n    not attribute((mark,type),M,line).\n\n% @soft(x_row) Positional interactions as suggested by Kim et al.\npreference(x_row,V) :-\n    entity(mark,V,M),\n    helper((mark,channel),M,x),\n    entity(facet,V,F),\n    attribute((facet,channel),F,row).\n\n% @soft(y_row) Positional interactions as suggested by Kim et al.\npreference(y_row,V) :-\n    entity(mark,V,M),\n    helper((mark,channel),M,y),\n    entity(facet,V,F),\n    attribute((facet,channel),F,row).\n\n% @soft(x_col) Positional interactions as suggested by Kim et al.\npreference(x_col,V) :-\n    entity(mark,V,M),\n    helper((mark,channel),M,x),\n    entity(facet,V,F),\n    attribute((facet,channel),F,col).\n\n% @soft(y_col) Positional interactions as suggested by Kim et al.\npreference(y_col,V) :-\n    entity(mark,V,M),\n    helper((mark,channel),M,y),\n    entity(facet,V,F),\n    attribute((facet,channel),F,col).\n\n% @soft(color_entropy_high) Entropy, primary quantive interactions as suggested by Kim et al.\npreference(color_entropy_high,E) :-\n    attribute((field,entropy),F,EN),\n    helper((encoding,field),E,F),\n    attribute((encoding,channel),E,color),\n    helper((encoding,scale_type),E,linear),\n    helper(enc_interesting,E),\n    EN > 3000.\n\n% @soft(color_entropy_low) Entropy, primary quantive interactions as suggested by Kim et al.\npreference(color_entropy_low,E) :-\n    attribute((field,entropy),F,EN),\n    helper((encoding,field),E,F),\n    attribute((encoding,channel),E,color),\n    helper((encoding,scale_type),E,linear),\n    helper(enc_interesting,E),\n    EN <= 3000.\n\n% @soft(size_entropy_high) Entropy, primary quantive interactions as suggested by Kim et al.\npreference(size_entropy_high,E) :-\n    attribute((field,entropy),F,EN),\n    helper((encoding,field),E,F),\n    attribute((encoding,channel),E,size),\n    helper((encoding,scale_type),E,linear),\n    helper(enc_interesting,E),\n    EN > 3000.\n\n% @soft(size_entropy_low) Entropy, primary quantive interactions as suggested by Kim et al.\npreference(size_entropy_low,E) :-\n    attribute((field,entropy),F,EN),\n    helper((encoding,field),E,F),\n    attribute((encoding,channel),E,size),\n    helper((encoding,scale_type),E,linear),\n    helper(enc_interesting,E),\n    EN <= 3000.\n\n% @soft(linear_scale) linear scale.\npreference(linear_scale,E) :-\n    helper((encoding,scale_type),E,linear).\n\n% @soft(log_scale) log scale.\npreference(log_scale,E) :-\n    helper((encoding,scale_type),E,log).\n\n% @soft(ordinal_scale) ordinal scale.\npreference(ordinal_scale,E) :-\n    helper((encoding,scale_type),E,ordinal).\n\n% @soft(categorical_scale) categorical scale.\npreference(categorical_scale,E) :-\n    helper((encoding,scale_type),E,categorical).\n\n% @soft(continuous_scale) Prefer to use linear, log or symlog scale for continuous variables.\npreference(continuous_scale,E) :-\n    helper((encoding,field),E,F),\n    attribute((field,type),F,(number;datetime)),\n    not helper((encoding,scale_type),E,linear),\n    not helper((encoding,scale_type),E,log),\n    not helper((encoding,scale_type),E,symlog).\n\n% @soft(skewed_not_log) Prefer log scale for positive skewed fields and symlog for fields that cross zero.\npreference(skewed_not_log,E) :-\n    helper((encoding,field),E,F),\n    helper(field_skewed,F),\n    attribute((field,min),F,MIN),\n    MIN > 0,\n    not helper((encoding,scale_type),E,log).\npreference(skewed_not_log,E) :-\n    helper((encoding,field),E,F),\n    helper(field_skewed,F),\n    attribute((field,min),F,MIN),\n    attribute((field,max),F,MAX),\n    MIN < 1,\n    MAX > 0,\n    not helper((encoding,scale_type),E,symlog).\n\n% @soft(c_c_point) Continuous by continuous for point mark.\npreference(c_c_point,M) :-\n    helper(is_c_c,M),\n    attribute((mark,type),M,point).\n\n% @soft(c_c_line) Continuous by continuous for line mark.\npreference(c_c_line,M) :-\n    helper(is_c_c,M),\n    attribute((mark,type),M,line).\n\n% @soft(c_c_area) Continuous by continuous for area mark.\npreference(c_c_area,M) :-\n    helper(is_c_c,M),\n    attribute((mark,type),M,area).\n\n% @soft(c_c_text) Continuous by continuous for text mark.\npreference(c_c_text,M) :-\n    helper(is_c_c,M),\n    attribute((mark,type),M,text).\n\n% @soft(c_d_overlap_point) Continuous by discrete for point mark.\npreference(c_d_overlap_point,M) :-\n    helper(is_c_d,M),\n    not helper(no_overlap,M),\n    attribute((mark,type),M,point).\n\n% @soft(c_d_overlap_bar) Continuous by discrete for bar mark.\npreference(c_d_overlap_bar,M) :-\n    helper(is_c_d,M),\n    not helper(no_overlap,M),\n    attribute((mark,type),M,bar).\n\n% @soft(c_d_overlap_line) Continuous by discrete for line mark.\npreference(c_d_overlap_line,M) :-\n    helper(is_c_d,M),\n    not helper(no_overlap,M),\n    attribute((mark,type),M,line).\n\n% @soft(c_d_overlap_area) Continuous by discrete for area mark.\npreference(c_d_overlap_area,M) :-\n    helper(is_c_d,M),\n    not helper(no_overlap,M),\n    attribute((mark,type),M,area).\n\n% @soft(c_d_overlap_text) Continuous by discrete for text mark.\npreference(c_d_overlap_text,M) :-\n    helper(is_c_d,M),\n    not helper(no_overlap,M),\n    attribute((mark,type),M,text).\n\n% @soft(c_d_overlap_tick) Continuous by discrete for tick mark.\npreference(c_d_overlap_tick,M) :-\n    helper(is_c_d,M),\n    not helper(no_overlap,M),\n    attribute((mark,type),M,tick).\n\n% @soft(c_d_no_overlap_point) Continuous by discrete for point mark.\npreference(c_d_no_overlap_point,M) :-\n    helper(is_c_d,M),\n    helper(no_overlap,M),\n    attribute((mark,type),M,point).\n\n% @soft(c_d_no_overlap_bar) Continuous by discrete for bar mark.\npreference(c_d_no_overlap_bar,M) :-\n    helper(is_c_d,M),\n    helper(no_overlap,M),\n    attribute((mark,type),M,bar).\n\n% @soft(c_d_no_overlap_line) Continuous by discrete for line mark.\npreference(c_d_no_overlap_line,M) :-\n    helper(is_c_d,M),\n    helper(no_overlap,M),\n    attribute((mark,type),M,line).\n\n% @soft(c_d_no_overlap_area) Continuous by discrete for area mark.\npreference(c_d_no_overlap_area,M) :-\n    helper(is_c_d,M),\n    helper(no_overlap,M),\n    attribute((mark,type),M,area).\n\n% @soft(c_d_no_overlap_text) Continuous by discrete for text mark.\npreference(c_d_no_overlap_text,M) :-\n    helper(is_c_d,M),\n    helper(no_overlap,M),\n    attribute((mark,type),M,text).\n\n% @soft(c_d_no_overlap_tick) Continuous by discrete for tick mark.\npreference(c_d_no_overlap_tick,M) :-\n    helper(is_c_d,M),\n    helper(no_overlap,M),\n    attribute((mark,type),M,tick).\n\n% @soft(tick_color) Prefer not to use color with tick marks as it is difficult to see.\npreference(tick_color,M) :-\n    attribute((mark,type),M,tick),\n    helper((mark,channel),M,color).\n\n% @soft(d_d_overlap) Prefer not to overlap with DxD.\npreference(d_d_overlap,M) :-\n    helper(is_d_d,M),\n    helper(overlap,M).\n\n% @soft(d_d_point) Discrete by discrete for point mark.\npreference(d_d_point,M) :-\n    helper(is_d_d,M),\n    attribute((mark,type),M,point).\n\n% @soft(d_d_text) Discrete by discrete for text mark.\npreference(d_d_text,M) :-\n    helper(is_d_d,M),\n    attribute((mark,type),M,text).\n\n% @soft(d_d_rect) Discrete by discrete for rect mark.\npreference(d_d_rect,M) :-\n    helper(is_d_d,M),\n    attribute((mark,type),M,rect).\n\n% @soft(linear_x) Linear scale with x channel.\npreference(linear_x,E) :-\n    attribute((encoding,channel),E,x),\n    helper((encoding,scale_type),E,linear).\n\n% @soft(linear_y) Linear scale with y channel.\npreference(linear_y,E) :-\n    attribute((encoding,channel),E,y),\n    helper((encoding,scale_type),E,linear).\n\n% @soft(linear_color) Linear scale with color channel.\npreference(linear_color,E) :-\n    attribute((encoding,channel),E,color),\n    helper((encoding,scale_type),E,linear).\n\n% @soft(linear_size) Linear scale with size channel.\npreference(linear_size,E) :-\n    attribute((encoding,channel),E,size),\n    helper((encoding,scale_type),E,linear).\n\n% @soft(linear_text) Linear scale with text channel.\npreference(linear_text,E) :-\n    attribute((encoding,channel),E,text),\n    helper((encoding,scale_type),E,linear).\n\n% @soft(log_x) Log scale with x channel.\npreference(log_x,E) :-\n    attribute((encoding,channel),E,x),\n    helper((encoding,scale_type),E,log).\n\n% @soft(log_y) Log scale with y channel.\npreference(log_y,E) :-\n    attribute((encoding,channel),E,y),\n    helper((encoding,scale_type),E,log).\n\n% @soft(log_color) Log scale with color channel.\npreference(log_color,E) :-\n    attribute((encoding,channel),E,color),\n    helper((encoding,scale_type),E,log).\n\n% @soft(log_size) Log scale with size channel.\npreference(log_size,E) :-\n    attribute((encoding,channel),E,size),\n    helper((encoding,scale_type),E,log).\n\n% @soft(log_text) Log scale with text channel.\npreference(log_text,E) :-\n    attribute((encoding,channel),E,text),\n    helper((encoding,scale_type),E,log).\n\n% @soft(ordinal_x) Ordinal scale with x channel.\npreference(ordinal_x,E) :-\n    attribute((encoding,channel),E,x),\n    helper((encoding,scale_type),E,ordinal).\n\n% @soft(ordinal_y) Ordinal scale with y channel.\npreference(ordinal_y,E) :-\n    attribute((encoding,channel),E,y),\n    helper((encoding,scale_type),E,ordinal).\n\n% @soft(ordinal_color) Ordinal scale with color channel.\npreference(ordinal_color,E) :-\n    attribute((encoding,channel),E,color),\n    helper((encoding,scale_type),E,ordinal).\n\n% @soft(ordinal_size) Ordinal scale with size channel.\npreference(ordinal_size,E) :-\n    attribute((encoding,channel),E,size),\n    helper((encoding,scale_type),E,ordinal).\n\n% @soft(ordinal_shape) Ordinal scale with shape channel.\npreference(ordinal_shape,E) :-\n    attribute((encoding,channel),E,shape),\n    helper((encoding,scale_type),E,ordinal).\n\n% @soft(ordinal_text) Ordinal scale with text channel.\npreference(ordinal_text,E) :-\n    attribute((encoding,channel),E,text),\n    helper((encoding,scale_type),E,ordinal).\n\n% @soft(ordinal_detail) Ordinal scale with detail channel.\npreference(ordinal_detail,E) :-\n    attribute((encoding,channel),E,detail),\n    helper((encoding,scale_type),E,ordinal).\n\n% @soft(categorical_color) Categorical scale with color channel.\npreference(categorical_color,E) :-\n    attribute((encoding,channel),E,color),\n    helper((encoding,scale_type),E,categorical).\n\n% @soft(aggregate_count) Count as aggregate op.\npreference(aggregate_count,E) :-\n    attribute((encoding,aggregate),E,count).\n\n% @soft(aggregate_mean) Mean as aggregate op.\npreference(aggregate_mean,E) :-\n    attribute((encoding,aggregate),E,mean).\n\n% @soft(aggregate_median) Median as aggregate op.\npreference(aggregate_median,E) :-\n    attribute((encoding,aggregate),E,median).\n\n% @soft(aggregate_min) Min as aggregate op.\npreference(aggregate_min,E) :-\n    attribute((encoding,aggregate),E,min).\n\n% @soft(aggregate_max) Max as aggregate op.\npreference(aggregate_max,E) :-\n    attribute((encoding,aggregate),E,max).\n\n% @soft(aggregate_stdev) Stdev as aggregate op.\npreference(aggregate_stdev,E) :-\n    attribute((encoding,aggregate),E,stdev).\n\n% @soft(aggregate_sum) Sum as aggregate op.\npreference(aggregate_sum,E) :-\n    attribute((encoding,aggregate),E,sum).\n\n% @soft(stack_zero) Zero base for stack op.\npreference(stack_zero,E) :-\n    attribute((encoding,stack),E,zero).\n\n% @soft(stack_center) Center groupbys as stack op.\npreference(stack_center,E) :-\n    attribute((encoding,stack),E,center).\n\n% @soft(stack_normalize) Normalize between groupbys as stack op.\npreference(stack_normalize,E) :-\n    attribute((encoding,stack),E,normalize).\n\n% @soft(value_point) Point mark for value tasks.\npreference(value_point,M) :-\n    attribute(task,root,value),\n    attribute((mark,type),M,point).\n\n% @soft(value_bar) Bar mark for value tasks.\npreference(value_bar,M) :-\n    attribute(task,root,value),\n    attribute((mark,type),M,bar).\n\n% @soft(value_line) Line mark for value tasks.\npreference(value_line,M) :-\n    attribute(task,root,value),\n    attribute((mark,type),M,line).\n\n% @soft(value_area) Area mark for value tasks.\npreference(value_area,M) :-\n    attribute(task,root,value),\n    attribute((mark,type),M,area).\n\n% @soft(value_text) Text mark for value tasks.\npreference(value_text,M) :-\n    attribute(task,root,value),\n    attribute((mark,type),M,text).\n\n% @soft(value_tick) Tick mark for value tasks.\npreference(value_tick,M) :-\n    attribute(task,root,value),\n    attribute((mark,type),M,tick).\n\n% @soft(value_rect) Rect mark for value tasks.\npreference(value_rect,M) :-\n    attribute(task,root,value),\n    attribute((mark,type),M,rect).\n\n% @soft(summary_point) Point mark for summary tasks.\npreference(summary_point,M) :-\n    attribute(task,root,summary),\n    attribute((mark,type),M,point).\n\n% @soft(summary_bar) Bar mark for summary tasks.\npreference(summary_bar,M) :-\n    attribute(task,root,summary),\n    attribute((mark,type),M,bar).\n\n% @soft(summary_line) Line mark for summary tasks.\npreference(summary_line,M) :-\n    attribute(task,root,summary),\n    attribute((mark,type),M,line).\n\n% @soft(summary_area) Area mark for summary tasks.\npreference(summary_area,M) :-\n    attribute(task,root,summary),\n    attribute((mark,type),M,area).\n\n% @soft(summary_text) Text mark for summary tasks.\npreference(summary_text,M) :-\n    attribute(task,root,summary),\n    attribute((mark,type),M,text).\n\n% @soft(summary_tick) Tick mark for summary tasks.\npreference(summary_tick,M) :-\n    attribute(task,root,summary),\n    attribute((mark,type),M,tick).\n\n% @soft(summary_rect) Rect mark for summary tasks.\npreference(summary_rect,M) :-\n    attribute(task,root,summary),\n    attribute((mark,type),M,rect).\n\n% @soft(value_continuous_x) Continuous x for value tasks.\npreference(value_continuous_x,E) :-\n    attribute(task,root,value),\n    attribute((encoding,channel),E,x),\n    not helper(mark_encoding_discrete_or_binned,_,E),\n    helper(enc_interesting,E).\n\n% @soft(value_continuous_y) Continuous y for value tasks.\npreference(value_continuous_y,E) :-\n    attribute(task,root,value),\n    attribute((encoding,channel),E,y),\n    not helper(mark_encoding_discrete_or_binned,_,E),\n    helper(enc_interesting,E).\n\n% @soft(value_continuous_color) Continuous color for value tasks.\npreference(value_continuous_color,E) :-\n    attribute(task,root,value),\n    attribute((encoding,channel),E,color),\n    not helper(mark_encoding_discrete_or_binned,_,E),\n    helper(enc_interesting,E).\n\n% @soft(value_continuous_size) Continuous size for value tasks.\npreference(value_continuous_size,E) :-\n    attribute(task,root,value),\n    attribute((encoding,channel),E,size),\n    not helper(mark_encoding_discrete_or_binned,_,E),\n    helper(enc_interesting,E).\n\n% @soft(value_continuous_text) Continuous text for value tasks.\npreference(value_continuous_text,E) :-\n    attribute(task,root,value),\n    attribute((encoding,channel),E,text),\n    not helper(mark_encoding_discrete_or_binned,_,E),\n    helper(enc_interesting,E).\n\n% @soft(value_discrete_x) Discrete x for value tasks.\npreference(value_discrete_x,E) :-\n    attribute(task,root,value),\n    attribute((encoding,channel),E,x),\n    helper(mark_encoding_discrete_or_binned,_,E),\n    helper(enc_interesting,E).\n\n% @soft(value_discrete_y) Discrete y for value tasks.\npreference(value_discrete_y,E) :-\n    attribute(task,root,value),\n    attribute((encoding,channel),E,y),\n    helper(mark_encoding_discrete_or_binned,_,E),\n    helper(enc_interesting,E).\n\n% @soft(value_discrete_color) Discrete color for value tasks.\npreference(value_discrete_color,E) :-\n    attribute(task,root,value),\n    attribute((encoding,channel),E,color),\n    helper(mark_encoding_discrete_or_binned,_,E),\n    helper(enc_interesting,E).\n\n% @soft(value_discrete_size) Discrete size for value tasks.\npreference(value_discrete_size,E) :-\n    attribute(task,root,value),\n    attribute((encoding,channel),E,size),\n    helper(mark_encoding_discrete_or_binned,_,E),\n    helper(enc_interesting,E).\n\n% @soft(value_discrete_shape) Discrete shape for value tasks.\npreference(value_discrete_shape,E) :-\n    attribute(task,root,value),\n    attribute((encoding,channel),E,shape),\n    helper(mark_encoding_discrete_or_binned,_,E),\n    helper(enc_interesting,E).\n\n% @soft(value_discrete_text) Discrete text for value tasks.\npreference(value_discrete_text,E) :-\n    attribute(task,root,value),\n    attribute((encoding,channel),E,text),\n    helper(mark_encoding_discrete_or_binned,_,E),\n    helper(enc_interesting,E).\n\n% @soft(value_discrete_detail) Discrete detail for value tasks.\npreference(value_discrete_detail,E) :-\n    attribute(task,root,value),\n    attribute((encoding,channel),E,detail),\n    helper(mark_encoding_discrete_or_binned,_,E),\n    helper(enc_interesting,E).\n\n% @soft(summary_continuous_x) Continuous x for summary tasks.\npreference(summary_continuous_x,E) :-\n    attribute(task,root,summary),\n    attribute((encoding,channel),E,x),\n    not helper(mark_encoding_discrete_or_binned,_,E),\n    helper(enc_interesting,E).\n\n% @soft(summary_continuous_y) Continuous y for summary tasks.\npreference(summary_continuous_y,E) :-\n    attribute(task,root,summary),\n    attribute((encoding,channel),E,y),\n    not helper(mark_encoding_discrete_or_binned,_,E),\n    helper(enc_interesting,E).\n\n% @soft(summary_continuous_color) Continuous color for summary tasks.\npreference(summary_continuous_color,E) :-\n    attribute(task,root,summary),\n    attribute((encoding,channel),E,color),\n    not helper(mark_encoding_discrete_or_binned,_,E),\n    helper(enc_interesting,E).\n\n% @soft(summary_continuous_size) Continuous size for summary tasks.\npreference(summary_continuous_size,E) :-\n    attribute(task,root,summary),\n    attribute((encoding,channel),E,size),\n    not helper(mark_encoding_discrete_or_binned,_,E),\n    helper(enc_interesting,E).\n\n% @soft(summary_continuous_text) Continuous text for summary tasks.\npreference(summary_continuous_text,E) :-\n    attribute(task,root,summary),\n    attribute((encoding,channel),E,text),\n    not helper(mark_encoding_discrete_or_binned,_,E),\n    helper(enc_interesting,E).\n\n% @soft(summary_discrete_x) Discrete x for summary tasks.\npreference(summary_discrete_x,E) :-\n    attribute(task,root,summary),\n    attribute((encoding,channel),E,x),\n    helper(mark_encoding_discrete_or_binned,_,E),\n    helper(enc_interesting,E).\n\n% @soft(summary_discrete_y) Discrete y for summary tasks.\npreference(summary_discrete_y,E) :-\n    attribute(task,root,summary),\n    attribute((encoding,channel),E,y),\n    helper(mark_encoding_discrete_or_binned,_,E),\n    helper(enc_interesting,E).\n\n% @soft(summary_discrete_color) Discrete color for summary tasks.\npreference(summary_discrete_color,E) :-\n    attribute(task,root,summary),\n    attribute((encoding,channel),E,color),\n    helper(mark_encoding_discrete_or_binned,_,E),\n    helper(enc_interesting,E).\n\n% @soft(summary_discrete_size) Discrete size for summary tasks.\npreference(summary_discrete_size,E) :-\n    attribute(task,root,summary),\n    attribute((encoding,channel),E,size),\n    helper(mark_encoding_discrete_or_binned,_,E),\n    helper(enc_interesting,E).\n\n% @soft(summary_discrete_shape) Discrete shape for summary tasks.\npreference(summary_discrete_shape,E) :-\n    attribute(task,root,summary),\n    attribute((encoding,channel),E,shape),\n    helper(mark_encoding_discrete_or_binned,_,E),\n    helper(enc_interesting,E).\n\n% @soft(summary_discrete_text) Discrete text for summary tasks.\npreference(summary_discrete_text,E) :-\n    attribute(task,root,summary),\n    attribute((encoding,channel),E,text),\n    helper(mark_encoding_discrete_or_binned,_,E),\n    helper(enc_interesting,E).\n\n% @soft(summary_discrete_detail) Discrete detail for summary tasks.\npreference(summary_discrete_detail,E) :-\n    attribute(task,root,summary),\n    attribute((encoding,channel),E,detail),\n    helper(mark_encoding_discrete_or_binned,_,E),\n    helper(enc_interesting,E).\n\n% @soft(interesting_x) Interesting on x channel.\npreference(interesting_x,E) :-\n    attribute((encoding,channel),E,x),\n    helper(enc_interesting,E).\n\n% @soft(interesting_y) Interesting on y channel.\npreference(interesting_y,E) :-\n    attribute((encoding,channel),E,y),\n    helper(enc_interesting,E).\n\n% @soft(interesting_color) Interesting on color channel.\npreference(interesting_color,E) :-\n    attribute((encoding,channel),E,color),\n    helper(enc_interesting,E).\n\n% @soft(interesting_size) Interesting on size channel.\npreference(interesting_size,E) :-\n    attribute((encoding,channel),E,size),\n    helper(enc_interesting,E).\n\n% @soft(interesting_shape) Interesting on shape channel.\npreference(interesting_shape,E) :-\n    attribute((encoding,channel),E,shape),\n    helper(enc_interesting,E).\n\n% @soft(interesting_text) Interesting on text channel.\npreference(interesting_text,E) :-\n    attribute((encoding,channel),E,text),\n    helper(enc_interesting,E).\n\n% @soft(interesting_row) Interesting on row channel.\npreference(interesting_row,E) :-\n    attribute((facet,channel),E,row),\n    helper(enc_interesting,E).\n\n% @soft(interesting_column) Interesting on column channel.\npreference(interesting_column,E) :-\n    attribute((facet,channel),E,col),\n    helper(enc_interesting,E).\n\n% @soft(interesting_detail) Interesting on detail channel.\npreference(interesting_detail,E) :-\n    attribute((encoding,channel),E,detail),\n    helper(enc_interesting,E).\n\n% @soft(cartesian_coordinate) Cartesian coordinates.\npreference(cartesian_coordinate,V) :-\n    attribute((view,coordinates),V,cartesian).\n\n% @soft(polar_coordinate) Polar coordinates.\npreference(polar_coordinate,V) :-\n    attribute((view,coordinates),V,polar).\n"
      },
      {
        "name": "optimize",
        "src": "% Minimize the feature weight\n\n#minimize { W,F,Q: preference_weight(F,W), preference(F,Q) }.\n"
      },
      {
        "name": "express",
        "src": "% ====== Field Control ======\n\n% @api(require(field, fieldname)) Guarantee that a field is visualized via encoding or facet\nviolation(field_not_visualized) :-\n    require(field, FieldName),\n    not attribute((encoding,field),_,FieldName),\n    not attribute((facet,field),_,FieldName).\n\n% @api(require((field, encoding), fieldname)) Guarantee that a specific field is used in an encoding\nviolation(field_not_in_encoding) :-\n    require((field, encoding), FieldName),\n    not attribute((encoding,field),_,FieldName).\n\n% @api(require((field, facet), fieldname)) Guarantee that a specific field is used in a facet\nviolation(field_not_in_facet) :-\n    require((field, facet), FieldName),\n    not attribute((facet,field),_,FieldName).\n\n% @api(forbid(field, fieldname)) Forbid a field from being used in any visualization\nviolation(forbidden_field_used) :-\n    forbid(field, FieldName),\n    attribute((encoding,field),_,FieldName).\n\nviolation(forbidden_field_used) :-\n    forbid(field, FieldName),\n    attribute((facet,field),_,FieldName).\n\n% @api(observed(field, fieldname)) Field is used (in encoding or facet)\nobserved(field, FieldName) :-\n    attribute((encoding,field),_,FieldName).\n\nobserved(field, FieldName) :-\n    attribute((facet,field),_,FieldName).\n\n% @api(observed((field, encoding), fieldname)) Field is used in an encoding\nobserved((field, encoding), FieldName) :-\n    attribute((encoding,field),_,FieldName).\n\n% @api(observed((field, facet), fieldname)) Field is used in a facet\nobserved((field, facet), FieldName) :-\n    attribute((facet,field),_,FieldName).\n\n% ====== Mark Control ======\n\n% @api(require(mark, marktype)) Guarantee that a specific mark type is used\nviolation(required_mark_missing) :-\n    require(mark, MarkType),\n    not attribute((mark,type),_,MarkType).\n\n% @api(forbid(mark, marktype)) Forbid a specific mark type from being used\nviolation(forbidden_mark_used) :-\n    forbid(mark, MarkType),\n    attribute((mark,type),_,MarkType).\n\n% @api(observed(mark, marktype)) Mark type is used\nobserved(mark, MarkType) :-\n    attribute((mark,type),_,MarkType).\n\n% ====== Channel Control ======\n\n% @api(require(channel, channelname)) Guarantee that a specific channel is used\nviolation(required_channel_missing) :-\n    require(channel, Channel),\n    not attribute((encoding,channel),_,Channel).\n\n% @api(forbid(channel, channelname)) Forbid a specific channel from being used\nviolation(forbidden_channel_used) :-\n    forbid(channel, Channel),\n    attribute((encoding,channel),_,Channel).\n\n% @api(observed(channel, channelname)) Channel is used\nobserved(channel, Channel) :-\n    attribute((encoding,channel),_,Channel).\n\n% ====== Facet Control ======\n\n% @api(require(facet)) Guarantee that faceting is used\nviolation(faceting_missing) :-\n    require(facet),\n    not entity(facet,_,_).\n\n% @api(forbid(facet)) Forbid any faceting\nviolation(faceting_forbidden) :-\n    forbid(facet),\n    entity(facet,_,_).\n\n% @api(require((facet, channel), channelname)) Require faceting on a specific channel (row/col)\nviolation(facet_channel_missing) :-\n    require((facet, channel), Channel),\n    not attribute((facet,channel),_,Channel).\n\n% @api(forbid((facet, channel), channelname)) Forbid faceting on a specific channel\nviolation(facet_channel_forbidden) :-\n    forbid((facet, channel), Channel),\n    attribute((facet,channel),_,Channel).\n\n% @api(observed(facet)) Faceting is used\nobserved(facet) :-\n    entity(facet,_,_).\n\n% @api(observed((facet, channel), channelname)) Specific facet channel is used\nobserved((facet, channel), Channel) :-\n    attribute((facet,channel),_,Channel).\n\n% ====== Binning Control ======\n\n% @api(require(binning)) Guarantee that at least one encoding uses binning\nviolation(binning_missing) :-\n    require(binning),\n    not attribute((encoding,binning),_,_).\n\n% @api(forbid(binning)) Forbid any binning\nviolation(binning_forbidden) :-\n    forbid(binning),\n    attribute((encoding,binning),_,_).\n\n% @api(require(binning, fieldname)) Require a specific field to be binned\nviolation(field_not_binned) :-\n    require(binning, FieldName),\n    attribute((encoding,field),E,FieldName),\n    not attribute((encoding,binning),E,_).\n\n% @api(forbid(binning, fieldname)) Forbid a specific field from being binned\nviolation(field_binned_forbidden) :-\n    forbid(binning, FieldName),\n    attribute((encoding,field),E,FieldName),\n    attribute((encoding,binning),E,_).\n\n% @api(observed(binning)) Binning is used\nobserved(binning) :-\n    attribute((encoding,binning),_,_).\n\n% @api(observed(binning, fieldname)) Specific field is binned\nobserved(binning, FieldName) :-\n    attribute((encoding,field),E,FieldName),\n    attribute((encoding,binning),E,_).\n\n% ====== Aggregation Control ======\n\n% @api(require(aggregate)) Guarantee that at least one encoding uses aggregation\nviolation(aggregation_missing) :-\n    require(aggregate),\n    not attribute((encoding,aggregate),_,_).\n\n% @api(forbid(aggregate)) Forbid any aggregation\nviolation(aggregation_forbidden) :-\n    forbid(aggregate),\n    attribute((encoding,aggregate),_,_).\n\n% @api(require(aggregate, aggtype)) Require a specific aggregation type to be used\nviolation(required_aggregate_missing) :-\n    require(aggregate, AggType),\n    not attribute((encoding,aggregate),_,AggType).\n\n% @api(forbid(aggregate, aggtype)) Forbid a specific aggregation type\nviolation(forbidden_aggregate_used) :-\n    forbid(aggregate, AggType),\n    attribute((encoding,aggregate),_,AggType).\n\n% @api(observed(aggregate)) Aggregation is used\nobserved(aggregate) :-\n    attribute((encoding,aggregate),_,_).\n\n% @api(observed(aggregate, aggtype)) Specific aggregation type is used\nobserved(aggregate, AggType) :-\n    attribute((encoding,aggregate),_,AggType).\n\n% ====== Scale Control ======\n\n% @api(require(scale, scaletype)) Require a specific scale type to be used\nviolation(required_scale_missing) :-\n    require(scale, ScaleType),\n    not attribute((scale,type),_,ScaleType).\n\n% @api(forbid(scale, scaletype)) Forbid a specific scale type\nviolation(forbidden_scale_used) :-\n    forbid(scale, ScaleType),\n    attribute((scale,type),_,ScaleType).\n\n% @api(require((scale, channel), scaletype)) Require a specific scale type for a specific channel\nviolation(required_channel_scale_missing) :-\n    require((scale, Channel), ScaleType),\n    entity(scale,_,S),\n    attribute((scale,channel),S,Channel),\n    not attribute((scale,type),S,ScaleType).\n\n% @api(forbid((scale, channel), scaletype)) Forbid a specific scale type for a specific channel\nviolation(forbidden_channel_scale_used) :-\n    forbid((scale, Channel), ScaleType),\n    entity(scale,_,S),\n    attribute((scale,channel),S,Channel),\n    attribute((scale,type),S,ScaleType).\n\n% @api(observed(scale, scaletype)) Scale type is used\nobserved(scale, ScaleType) :-\n    attribute((scale,type),_,ScaleType).\n\n% @api(observed((scale, channel), scaletype)) Specific scale type used on specific channel\nobserved((scale, Channel), ScaleType) :-\n    entity(scale,_,S),\n    attribute((scale,channel),S,Channel),\n    attribute((scale,type),S,ScaleType).\n\n% ====== Stack Control ======\n\n% @api(require(stack)) Guarantee that stacking is used\nviolation(stacking_missing) :-\n    require(stack),\n    not attribute((encoding,stack),_,_).\n\n% @api(forbid(stack)) Forbid any stacking\nviolation(stacking_forbidden) :-\n    forbid(stack),\n    attribute((encoding,stack),_,_).\n\n% @api(require(stack, stackmethod)) Require a specific stacking method\nviolation(required_stack_method_missing) :-\n    require(stack, StackMethod),\n    not attribute((encoding,stack),_,StackMethod).\n\n% @api(forbid(stack, stackmethod)) Forbid a specific stacking method\nviolation(forbidden_stack_method_used) :-\n    forbid(stack, StackMethod),\n    attribute((encoding,stack),_,StackMethod).\n\n% @api(observed(stack)) Stacking is used\nobserved(stack) :-\n    attribute((encoding,stack),_,_).\n\n% @api(observed(stack, stackmethod)) Specific stack method is used\nobserved(stack, StackMethod) :-\n    attribute((encoding,stack),_,StackMethod).\n\n% ====== Definition Declarations ======\n% Tell Clingo these predicates are defined externally to silence \"atom does not occur in any rule head\" warnings.\n\n#defined require/1.\n#defined require/2.\n#defined forbid/1.\n#defined forbid/2.\n"
      },
      {
        "name": "spec",
        "src": "attribute(number_rows,root,76).\nentity(field,root,0).\nattribute((field,name),0,publication_year).\nattribute((field,type),0,datetime).\nattribute((field,unique),0,35).\nattribute((field,entropy),0,3480).\nentity(field,root,1).\nattribute((field,name),1,conference).\nattribute((field,type),1,string).\nattribute((field,unique),1,4).\nattribute((field,entropy),1,1307).\nattribute((field,freq),1,26).\nattribute((field,min_length),1,3).\nattribute((field,max_length),1,7).\nentity(field,root,2).\nattribute((field,name),2,paper_count).\nattribute((field,type),2,number).\nattribute((field,unique),2,51).\nattribute((field,entropy),2,3820).\nattribute((field,min),2,16).\nattribute((field,max),2,133).\nattribute((field,std),2,24).\nattribute((field,skew),2,1).\nentity(view,root,v0).\nentity(mark,v0,m0).\nrequire(field, publication_year).\nrequire(field, conference).\nrequire(field, paper_count).\nattribute(task,root,summary)."
      }
    ],
    "weights": [
      {
        "feature": "aggregate",
        "weight": 2
      },
      {
        "feature": "bin",
        "weight": 4
      },
      {
        "feature": "bin_high",
        "weight": 10
      },
      ...
    ]
  }
]

The dump itself is represented as a Narwhals dataframe, therefore it can be easily exported / imported e.g. using parquet format.

The DracoExpress API allows the perfect reconstruction of Draco objects at runtime from a dumped state's program and weights, opening opportunities for a mixture-of-experts use case, where applications can route requests to an "ideal" Draco instance. However, for most use cases switching the weights dynamically should also suffice.

Weight and rule improvements

Adds a variety of new rules to steer core Draco towards sensible recommendations by default even when starting out from extremely minimal specs, in which we only define which fields we require in the visualization.

Extend computed data schema

  • skew allows us to reason about usage of log / symlog / linear scale & e.g. whether mean or median is better aggregation
  • string min_length and max_length allow us to reason about spacing, label angles and chart orientation

Renderer refactoring & improvement

  • extracted common utilities from Altair renderer to facilitate work on a Mosaic-spec-based renderer
  • render method now optionally accepts a mapping from technical field names to human-readable strings
  • we generate tooltips for charts rendered with Altair renderer

Also includes various fixes to remedy some paper-cuts.

WIP. Drafting this PR to facilitate discussions with @domoritz

peter-gy added 30 commits July 21, 2025 14:55
`vertical_scrolling_y:` Just as we don't want horiztonal scrolling, we also don't want vertical scrolling.

`vertical_scrolling_row`: Without this, we recommend row-faceted charts over fields of 1k+ cardinality, killing vega lite in the process.
Adds a heavy penalty for faceting over `N > 12` cardinality field.
Helps us reduce cases where Vega Lite rendering is broken due to choosing a field with 1k+ unique values to facet over.
We don't want to use same mark to encode different fields and we also don't want to overload same channel in the same view.
- increase weight
- increase threshold from 15 to 40 so that we don't start binning too "soon"
Advocates for using categorical scale for low-cardinality fields
Preference to use line mark together with datetime field
Allows us to recommend log scale for highly skewed numeric fields
Prefer using log scale for skewed fields with positive values
- Introduced `total_facet_cardinality` helper to calculate the total cardinality of facets in a view.
- Updated `facet_used_before_color` preference to consider numeric field cardinality.
- Added `underplotting` preference to avoid faceting with too few data points.
- Implemented `continuous_scale` preference for linear, log, or symlog scales on continuous variables.
- Adjusted weights for various preferences to reflect new logic.
…ggregate_weight`

- Removed cardinality check for numeric fields in `facet_used_before_color` preference.
- Increased `aggregate_weight` from 1 to 2 to reflect updated logic.
Also modified `bin` to consider binning within facets
…al data

- Introduced preferences to prefer faceting over color for line/point marks and to avoid using ordinal scale with color for high cardinality categorical data.
- Added corresponding weights for the new preferences to ensure proper prioritization.
When task is `value`, binning is counter-productive
…ore facet

- Introduced a new preference to prioritize faceting on both row and column before using color for point marks with high cardinality to mitigate overplotting.
- Added corresponding weight to ensure proper prioritization of this preference.
peter-gy added 5 commits July 29, 2025 16:15
Allows us to set label spacing, etc. as a function of min and max of string field
- Changed the skew threshold for determining if a field is skewed from 5 to 2.
- Updated weights for row and column facets, setting `facet_row_weight` to 0 and `facet_col_weight` to 1 to prioritize row facets.
peter-gy added 8 commits July 31, 2025 14:41
Introduced a new preference to avoid using text channels for fields with high cardinality and updated the corresponding weight to prioritize this preference in the layout.
add preferences for line mark with multiple number fields and color encoding for point mark
@openhands-ai
Copy link

openhands-ai bot commented Aug 13, 2025

Looks like there are a few issues preventing this PR from being merged!

  • GitHub Actions are failing:
    • Test

If you'd like me to help, just leave a comment, like

@OpenHands please fix the failing actions on PR #949 at branch peter-gy/dracox

Feel free to include any additional details that might help me get this PR into a better state.

You can manage your notification settings

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants