Changing vega lite markLine to markBar makes it loose sort order

178 Views Asked by At

I have made following visualization:

data = [{"student_name": "student 0", "e": "100.15", "d": "127.81"}, {"student_name": "student 1", "e": "100.30", "d": "189.94"}, {"student_name": "student 2", "e": "100.15", "d": "105.33"}, {"student_name": "student 3", "e": "99.41", "d": "85.36"}, {"student_name": "student 4", "e": "100.00", "d": "203.70"}, {"student_name": "student 5", "e": "100.15", "d": "139.05"}, {"student_name": "student 6", "e": "41.72", "d": "41.72"}, {"student_name": "student 7", "e": "100.30", "d": "260.50"}, {"student_name": "student 8", "e": "92.60", "d": "94.53"}, {"student_name": "student 9", "e": "99.11", "d": "86.39"}, {"student_name": "student 10", "e": "97.49", "d": "92.46"}, {"student_name": "student 11", "e": "98.37", "d": "83.58"}, {"student_name": "student 12", "e": "100.15", "d": "182.40"}, {"student_name": "student 13", "e": "100.15", "d": "99.41"}, {"student_name": "student 14", "e": "86.69", "d": "81.95"}, {"student_name": "student 15", "e": "98.08", "d": "306.51"}, {"student_name": "student 16", "e": "100.15", "d": "86.54"}, {"student_name": "student 17", "e": "100.15", "d": "186.39"}, {"student_name": "student 18", "e": "99.26", "d": "93.64"}, {"student_name": "student 19", "e": "100.15", "d": "102.66"}, {"student_name": "student 20", "e": "95.71", "d": "52.96"}, {"student_name": "student 21", "e": "99.85", "d": "99.41"}, {"student_name": "student 22", "e": "98.96", "d": "100.44"}, {"student_name": "student 23", "e": "100.15", "d": "131.07"}, {"student_name": "student 24", "e": "99.56", "d": "76.92"}, {"student_name": "student 25", "e": "100.15", "d": "213.46"}, {"student_name": "student 26", "e": "100.15", "d": "311.24"}, {"student_name": "student 27", "e": "100.15", "d": "21.89"}, {"student_name": "student 28", "e": "96.60", "d": "6.36"}, {"student_name": "student 29", "e": "53.70", "d": "3.70"}, {"student_name": "student 30", "e": "96.75", "d": "46.60"}, {"student_name": "student 31", "e": "100.15", "d": "100.15"}, {"student_name": "student 32", "e": "100.30", "d": "115.68"}, {"student_name": "student 33", "e": "87.13", "d": "103.85"}, {"student_name": "student 34", "e": "100.15", "d": "104.14"}]

createChart = function (data) {
  max_d = d3.max(data, record => parseFloat(record.d));
  max_e = d3.max(data, record => parseFloat(record.e));
  max_y_scale_value_for_d = d3.max([100, max_d]);
  max_y_scale_value_for_e = d3.max([100, max_e]);

  const mpg = vl.markLine()
              .data(data)
              .transform([{ "calculate": "toNumber(datum.d)", "as": "d2" }, { "calculate": "toNumber(datum.e)", "as": "e2" }])
              
              .encode(
                      vl.x()
                        .fieldN('student_name')
                        .title('students')
                        .axis({ labels: false, ticks: false })
                        .sort('-y'),
                      vl.y()
                        .fieldQ('d2')
                        .scale({ "domain": [0, max_y_scale_value_for_d] })
                        .title('D'),        
                      vl.tooltip([{ 'field': 'student_name', 'title': 'Sudent Name' },
                                  { 'field': 'd', 'title': 'D' },
                                  { 'field': 'e', 'title': 'E' }])
                                ).width(500).height(250);

  const hp = mpg.markLine({ color: '#227a15' })     
              .encode(
                      vl.y()
                        .fieldQ('e2')
                        .scale({ "domain": [0, max_y_scale_value_for_e] })
                        .title('E')
                     );

  const plot = vl.vconcat(
                       vl.layer(mpg, mpg.markCircle()),
                       vl.layer(hp, hp.markCircle({ color: '#227a15' }))
                     )

  return plot.toObject();
}

const chart_spec_json = this.createChart(data)
const opt = {
               renderer: "canvas",
               actions: false
            };
vegaEmbed("#stats", chart_spec_json, opt);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<script src="https://unpkg.com/[email protected]/build/vega.min.js"></script>
<script src="https://unpkg.com/[email protected]/build/vega-lite.min.js"></script>
<script src="https://www.unpkg.com/[email protected]/build/vega-embed.min.js"></script>
<script src="https://unpkg.com/[email protected]/build/vega-lite-api.min.js"></script>
<div id="stats" />

It renders as follows (run above script to see it in action):

enter image description here

But when I change markLine() to mmarkBar(), the top blue D graph looses its sort order with the warning WARN Domains that should be unioned has conflicting sort properties. Sort will be set to true.:

enter image description here

data = [{"student_name": "student 0", "e": "100.15", "d": "127.81"}, {"student_name": "student 1", "e": "100.30", "d": "189.94"}, {"student_name": "student 2", "e": "100.15", "d": "105.33"}, {"student_name": "student 3", "e": "99.41", "d": "85.36"}, {"student_name": "student 4", "e": "100.00", "d": "203.70"}, {"student_name": "student 5", "e": "100.15", "d": "139.05"}, {"student_name": "student 6", "e": "41.72", "d": "41.72"}, {"student_name": "student 7", "e": "100.30", "d": "260.50"}, {"student_name": "student 8", "e": "92.60", "d": "94.53"}, {"student_name": "student 9", "e": "99.11", "d": "86.39"}, {"student_name": "student 10", "e": "97.49", "d": "92.46"}, {"student_name": "student 11", "e": "98.37", "d": "83.58"}, {"student_name": "student 12", "e": "100.15", "d": "182.40"}, {"student_name": "student 13", "e": "100.15", "d": "99.41"}, {"student_name": "student 14", "e": "86.69", "d": "81.95"}, {"student_name": "student 15", "e": "98.08", "d": "306.51"}, {"student_name": "student 16", "e": "100.15", "d": "86.54"}, {"student_name": "student 17", "e": "100.15", "d": "186.39"}, {"student_name": "student 18", "e": "99.26", "d": "93.64"}, {"student_name": "student 19", "e": "100.15", "d": "102.66"}, {"student_name": "student 20", "e": "95.71", "d": "52.96"}, {"student_name": "student 21", "e": "99.85", "d": "99.41"}, {"student_name": "student 22", "e": "98.96", "d": "100.44"}, {"student_name": "student 23", "e": "100.15", "d": "131.07"}, {"student_name": "student 24", "e": "99.56", "d": "76.92"}, {"student_name": "student 25", "e": "100.15", "d": "213.46"}, {"student_name": "student 26", "e": "100.15", "d": "311.24"}, {"student_name": "student 27", "e": "100.15", "d": "21.89"}, {"student_name": "student 28", "e": "96.60", "d": "6.36"}, {"student_name": "student 29", "e": "53.70", "d": "3.70"}, {"student_name": "student 30", "e": "96.75", "d": "46.60"}, {"student_name": "student 31", "e": "100.15", "d": "100.15"}, {"student_name": "student 32", "e": "100.30", "d": "115.68"}, {"student_name": "student 33", "e": "87.13", "d": "103.85"}, {"student_name": "student 34", "e": "100.15", "d": "104.14"}]

createChart = function (data) {
  max_d = d3.max(data, record => parseFloat(record.d));
  max_e = d3.max(data, record => parseFloat(record.e));
  max_y_scale_value_for_d = d3.max([100, max_d]);
  max_y_scale_value_for_e = d3.max([100, max_e]);

    const mpg = vl.markBar()
              .data(data)
              .transform([{ "calculate": "toNumber(datum.d)", "as": "d2" }, { "calculate": "toNumber(datum.e)", "as": "e2" }])
              
              .encode(
                      vl.x()
                        .fieldN('student_name')
                        .title('students')
                        .axis({ labels: false, ticks: false })
                        .sort('-y'),
                      vl.y()
                        .fieldQ('d2')
                        .scale({ "domain": [0, max_y_scale_value_for_d] })
                        .title('D'),        
                      vl.tooltip([{ 'field': 'student_name', 'title': 'Sudent Name' },
                                  { 'field': 'd', 'title': 'D' },
                                  { 'field': 'e', 'title': 'E' }])
                                ).width(500).height(250);

  const hp = mpg.markLine({ color: '#227a15' })     
              .encode(
                      vl.y()
                        .fieldQ('e2')
                        .scale({ "domain": [0, max_y_scale_value_for_e] })
                        .title('E')
                     );

  const plot = vl.vconcat(
                       vl.layer(mpg, mpg.markCircle()),
                       vl.layer(hp, hp.markCircle({ color: '#227a15' }))
                     )
  return plot.toObject();
}

const chart_spec_json = this.createChart(data)
const opt = {
               renderer: "canvas",
               actions: false
            };
vegaEmbed("#stats", chart_spec_json, opt);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<script src="https://unpkg.com/[email protected]/build/vega.min.js"></script>
<script src="https://unpkg.com/[email protected]/build/vega-lite.min.js"></script>
<script src="https://www.unpkg.com/[email protected]/build/vega-embed.min.js"></script>
<script src="https://unpkg.com/[email protected]/build/vega-lite-api.min.js"></script>
<div id="stats" />

Run above script to see markBar() loose sort order. Here is the corresponding observable notebook.

Why is it so?

2

There are 2 best solutions below

9
Stas S On BEST ANSWER

Try to use markRect instead of markBar, or remove markCircle on top of markBar (do you really need circles on top of bars?)

The issue with markBar and markCircle, is that under the hood vega-lite creates a separate dataset for markBar, which might seem excessive for your case, but it becomes more useful if you'd have stacked bars. Then it tries to merge them, but it doesn't like if they both have sort order, despite it is the same order:

if (isDataRefDomain(domain) && sorts.length > 0) {
      let sort = sorts[0];
      if (sorts.length > 1) {
        log.warn(log.message.MORE_THAN_ONE_SORT);
        sort = true;

I believe vega-lite can do a better job, checking if those sorts are the same or not, instead of checking just count, but in the meantime you can either use markRect which doesn't create this additional dataset, or not use markCircle at all.

Also I would recommend generating vega-lite spec from vega-lite-api, by calling plot.toSpec() and pasting it to Vega Editor, this way you'll be able to see compiled vega spec and all the auxiliary datasets generated by vega-lite.

1
davidebacci On

You are trying to sort by -y but you have two different y encodings specified and only one x encoding. Can you try the following to see if it gives you what you want?

enter image description here

{
    let data = [{"student_name": "student 0", "e": "100.15", "d": "127.81"}, {"student_name": "student 1", "e": "100.30", "d": "189.94"}, {"student_name": "student 2", "e": "100.15", "d": "105.33"}, {"student_name": "student 3", "e": "99.41", "d": "85.36"}, {"student_name": "student 4", "e": "100.00", "d": "203.70"}, {"student_name": "student 5", "e": "100.15", "d": "139.05"}, {"student_name": "student 6", "e": "41.72", "d": "41.72"}, {"student_name": "student 7", "e": "100.30", "d": "260.50"}, {"student_name": "student 8", "e": "92.60", "d": "94.53"}, {"student_name": "student 9", "e": "99.11", "d": "86.39"}, {"student_name": "student 10", "e": "97.49", "d": "92.46"}, {"student_name": "student 11", "e": "98.37", "d": "83.58"}, {"student_name": "student 12", "e": "100.15", "d": "182.40"}, {"student_name": "student 13", "e": "100.15", "d": "99.41"}, {"student_name": "student 14", "e": "86.69", "d": "81.95"}, {"student_name": "student 15", "e": "98.08", "d": "306.51"}, {"student_name": "student 16", "e": "100.15", "d": "86.54"}, {"student_name": "student 17", "e": "100.15", "d": "186.39"}, {"student_name": "student 18", "e": "99.26", "d": "93.64"}, {"student_name": "student 19", "e": "100.15", "d": "102.66"}, {"student_name": "student 20", "e": "95.71", "d": "52.96"}, {"student_name": "student 21", "e": "99.85", "d": "99.41"}, {"student_name": "student 22", "e": "98.96", "d": "100.44"}, {"student_name": "student 23", "e": "100.15", "d": "131.07"}, {"student_name": "student 24", "e": "99.56", "d": "76.92"}, {"student_name": "student 25", "e": "100.15", "d": "213.46"}, {"student_name": "student 26", "e": "100.15", "d": "311.24"}, {"student_name": "student 27", "e": "100.15", "d": "21.89"}, {"student_name": "student 28", "e": "96.60", "d": "6.36"}, {"student_name": "student 29", "e": "53.70", "d": "3.70"}, {"student_name": "student 30", "e": "96.75", "d": "46.60"}, {"student_name": "student 31", "e": "100.15", "d": "100.15"}, {"student_name": "student 32", "e": "100.30", "d": "115.68"}, {"student_name": "student 33", "e": "87.13", "d": "103.85"}, {"student_name": "student 34", "e": "100.15", "d": "104.14"}]
  let max_d = d3.max(data, record => parseFloat(record.d));
    let max_e = d3.max(data, record => parseFloat(record.e));
    let max_y_scale_value_for_d = d3.max([100, max_d]);
    let max_y_scale_value_for_e = d3.max([100, max_e]);

  const mpg = vl.markBar()
              .data(data)
              .transform([{ "calculate": "toNumber(datum.d)", "as": "d2" }, { "calculate": "toNumber(datum.e)", "as": "e2" }])
              
              .encode(
                      vl.x()
                        .fieldN('student_name')
                        .title('students')
                        .axis({ labels: false, ticks: false })
                        .sort({field:'d2', op:"sum", order:"descending"}),
                      vl.y()
                        .fieldQ('d2')
                        .scale({ "domain": [0, max_y_scale_value_for_d] })
                        .title('D'),        
                      vl.tooltip([{ 'field': 'student_name', 'title': 'Sudent Name' },
                                  { 'field': 'd', 'title': 'D' },
                                  { 'field': 'e', 'title': 'E' }])
                                ).width(500).height(250);

  const hp = mpg.markLine({ color: '#227a15' })     
              .encode(
                vl.x()
                        .fieldN('student_name')
                        .title('students')
                        .axis({ labels: false, ticks: false })
                        .sort('-y'),
                      vl.y()
                        .fieldQ('e2')
                        .scale({ "domain": [0, max_y_scale_value_for_e] })
                        .title('E')
                     );

  const plot = vl.vconcat(
                       vl.layer(mpg, mpg.markCircle()),
                       vl.layer(hp, hp.markCircle({ color: '#227a15' }))
                     )
  
  return plot.render();
}