Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ordering for ordinal and continuous variables along vertical axes #16

Open
anobel opened this issue Jan 6, 2016 · 17 comments
Open

ordering for ordinal and continuous variables along vertical axes #16

anobel opened this issue Jan 6, 2016 · 17 comments

Comments

@anobel
Copy link

anobel commented Jan 6, 2016

Is it possible to change the ordering of values along the various Y-axes in a parcoords plot?

  • For a categorical factor variable, it seems that when the plot is rendered, it is in descending order of frequency and does not respect the ordering of the factor variable. Is there a way to pass that through?
  • Similarly, it appears that all continuous values are organized in descending order. Is it possible to vary that, on an axis by axis basis?

This is achieved in d3, as I understand, by passing d3.scale.linear().range(); such as:

var dimensions = [
  {
    name: "name",
    scale: d3.scale.ordinal().rangePoints([0, height]),
    type: "string"
  },
  {
    name: "economy (mpg)",
    scale: d3.scale.linear().range([0, height]),
    type: "number"
  }
]
@timelyportfolio
Copy link
Owner

Don't remember. I'll have a look. Most likely related to syntagmatic/parallel-coordinates#90.

@timelyportfolio
Copy link
Owner

Unfortunately, @anobel, this is not currently available. I hope it gets added soon. I'll continue to play to see if I can work it in.

@anobel
Copy link
Author

anobel commented Jan 6, 2016

also discussed here: syntagmatic/parallel-coordinates#106

@timelyportfolio
Copy link
Owner

and I'm so excited we just got a pull request. I'll check it out and merge it in an experimental spot for us to play.

@timelyportfolio
Copy link
Owner

@anobel, I was able to incorporate the pull request. The code currently is very messy as I try to think through patterns and best ease of use. Things like tickSize, tickFormat, tickValues, and title are really easy and can be provided through a new dimensions argument.

# devtools::install_github("timelyportfolio/parcoords@feature/dimensions")

library(parcoords)

parcoords(
  mtcars,
  dimensions = list(
    cyl = list(
      title = "cylinder",
      tickValues = unique(mtcars$cyl)
    )
  )
)

What gets tricky/hacky is providing a custom scale, and the difficulty arises from R to JS conversion and order of operations. However, using tasks we can accomplish this, but unfortunately will require two parcoords.render() which with really large data might prove troublesome.

# devtools::install_github("timelyportfolio/parcoords@feature/dimensions")

library(parcoords)

parcoords(
  mtcars,
  brushMode = "2d",
  #reorderable = TRUE,
  dimensions = list(
    cyl = list(
      tickValues = c(4,6,8)
    )
  ),
  tasks = list(
    htmlwidgets::JS(sprintf(
"
function(){
  debugger
  this.parcoords.dimensions()['names']
      .yscale = d3.scale.ordinal()
        .domain([%s])
        .rangePoints([
          1,
          this.parcoords.height()-this.parcoords.margin().top - this.parcoords.margin().bottom
        ])

  // reverse order of cylinders
  this.parcoords.dimensions()['cyl']
      .yscale
      .domain(
        this.parcoords.dimensions()['cyl'].yscale.domain().reverse()
      );

  this.parcoords.render()

  // duplicated from the widget js code
  //  to make sure reorderable and brushes work
  if( this.x.options.reorderable ) {
    this.parcoords.reorderable();
  } else {
    this.parcoords.createAxes();
  }

  if( this.x.options.brushMode ) {
    this.parcoords.brushMode(this.x.options.brushMode);
    this.parcoords.brushPredicate(this.x.options.brushPredicate);
  }

  // delete title from the rownames axis
  d3.select('#' + this.el.id + ' .dimension .axis > text').remove();
}
"     ,
      paste0(sort(shQuote(rownames(mtcars))),collapse=",")
    ))
  )
)

@anobel
Copy link
Author

anobel commented Jan 7, 2016

@timelyportfolio, thanks so much for putting this together. I'm working through your examples and so far I've come across one issue: it seems the new dimensions option is not compatible with rownames=F

For example,

parcoords(
  mtcars
  ,rownames=T    
  ,dimensions = list(
    cyl = list(
      title = "cylinder",
      tickValues = unique(mtcars$cyl)
    )
  )
)

Generates:
rownamest

whereas

parcoords(
  mtcars
  ,rownames = F
  ,dimensions = list(
    cyl = list(
      title = "cylinder",
      tickValues = unique(mtcars$cyl)
    )
  )
)

Generates:
rownamesf

@anobel
Copy link
Author

anobel commented Jan 7, 2016

in addition, running into a problem with tick marks.

with the following code:

parcoords(pcfig 
          # ,rownames=F,
          ,brushMode = "1d-axes"
          ,alpha=0.15
          ,axisDots = F
          ,reorderable = T
          ,queue = F, 
          ,color = list(
            colorBy = "Firstline Agent",
            colorScale = htmlwidgets::JS("d3.scale.category10()"))
          ,dimensions = list(
            timetotreat = list(
              tickValues = c(0,1,2,3,4)
            )
          )
          ,tasks = list(
            htmlwidgets::JS(sprintf(
              "
              function(){
              debugger
              this.parcoords.dimensions()['names']
              .yscale = d3.scale.ordinal()
              .domain([%s])
              .rangePoints([
              1,
              this.parcoords.height()-this.parcoords.margin().top - this.parcoords.margin().bottom
              ])

              // reverse order of cylinders
              this.parcoords.dimensions()['timetotreat']
              .yscale
              .domain(
              this.parcoords.dimensions()['timetotreat'].yscale.domain().reverse()
              );

              this.parcoords.render()

              // duplicated from the widget js code
              //  to make sure reorderable and brushes work
              if( this.x.options.reorderable ) {
              this.parcoords.reorderable();
              } else {
              this.parcoords.createAxes();
              }

              if( this.x.options.brushMode ) {
              this.parcoords.brushMode(this.x.options.brushMode);
              this.parcoords.brushPredicate(this.x.options.brushPredicate);
              }

              // delete title from the rownames axis
              d3.select('#' + this.el.id + ' .dimension .axis > text').remove();
              }
              "
              ,paste0(sort(shQuote(rownames(pcfig))),collapse=",")
            ))
          )
)

almost everything works, except the tick mark labels are in descending order but the observation lines are drawn as if it were in ascending order.
when I set reorderable=F, the axis labels and lines are both correct (ascending order), but row names show up and the entire figure loses interactivity

@timelyportfolio
Copy link
Owner

Thanks @anobel for all the feedback/reports. It seems the hideAxis is a bug in the source parallel-coordinates js, but I can work around in the parcoords.js which I just committed if you don't mind reinstalling.

I missed one step to make reorderable work and also had to fix I think a "bug" in the source library. Try this code which adds this.parcoords.removeAxes() and with reinstall gets the patched parallel-coordinates.

Commits 998806a and 54f82dd made the fixes.

parcoords(pcfig 
          # ,rownames=F,
          ,brushMode = "1d-axes"
          ,alpha=0.15
          ,axisDots = F
          ,reorderable = T
          ,queue = F, 
          ,color = list(
            colorBy = "Firstline Agent",
            colorScale = htmlwidgets::JS("d3.scale.category10()"))
          ,dimensions = list(
            timetotreat = list(
              tickValues = c(0,1,2,3,4)
            )
          )
          ,tasks = list(
            htmlwidgets::JS(sprintf(
              "
              function(){
              debugger
              this.parcoords.dimensions()['names']
              .yscale = d3.scale.ordinal()
              .domain([%s])
              .rangePoints([
              1,
              this.parcoords.height()-this.parcoords.margin().top - this.parcoords.margin().bottom
              ])

              // reverse order of cylinders
              this.parcoords.dimensions()['timetotreat']
              .yscale
              .domain(
              this.parcoords.dimensions()['timetotreat'].yscale.domain().reverse()
              );

              this.parcoords.removeAxes();
              this.parcoords.render();

              // duplicated from the widget js code
              //  to make sure reorderable and brushes work
              if( this.x.options.reorderable ) {
              this.parcoords.reorderable();
              } else {
              this.parcoords.createAxes();
              }

              if( this.x.options.brushMode ) {
              this.parcoords.brushMode(this.x.options.brushMode);
              this.parcoords.brushPredicate(this.x.options.brushPredicate);
              }

              // delete title from the rownames axis
              d3.select('#' + this.el.id + ' .dimension .axis > text').remove();
              }
              "
              ,paste0(sort(shQuote(rownames(pcfig))),collapse=",")
            ))
          )
)

@anobel
Copy link
Author

anobel commented Jan 7, 2016

I reinstalled the latest parcoords dimensions branch, and using the code above, but it didn't seem to make any impact. With rownames=T I lose all interactivity (but axis is reversed, as desired), and with rownames=F the axis is not reversed but the interactivity works....
not sure what I'm doing wrong

@timelyportfolio
Copy link
Owner

@anobel, Ok, I think the problem with rownames = F is that the example code

              this.parcoords.dimensions()['names']
              .yscale = d3.scale.ordinal()
              .domain([%s])
              .rangePoints([
              1,
              this.parcoords.height()-this.parcoords.margin().top - this.parcoords.margin().bottom
              ])

expects names which now won't exist if rownames=FALSE with the bugfix that I just committed. I hope (fingers-crossed) removing that bit will fix that problem. Let me know.

I'll keep working on the other.

@timelyportfolio
Copy link
Owner

@anobel, with rownames = TRUE do you see any errors in your console (F12 in Chrome) or right-click-> Inspect in RStudio?

@timelyportfolio
Copy link
Owner

@anobel, I see the problem arises with 1D, and to fix we need to reset the brushes. Try this code. Sorry for all this, but I guess these are the dangers of living on the bleeding edge :)

parcoords(
  mtcars,
  rownames = TRUE,
  brushMode = "1d",
  reorderable = TRUE,
  dimensions = list(
    cyl = list(
      tickValues = c(4,6,8)
    )
  ),
  tasks = list(
    htmlwidgets::JS(
"
function(){
  debugger
  // reverse order of cylinders
  this.parcoords.dimensions()['cyl']
      .yscale
      .domain(
        this.parcoords.dimensions()['cyl'].yscale.domain().reverse()
      );

  this.parcoords.removeAxes();
  this.parcoords.render();

  // duplicated from the widget js code
  //  to make sure reorderable and brushes work
  if( this.x.options.reorderable ) {
    this.parcoords.reorderable();
  } else {
    this.parcoords.createAxes();
  }

  if( this.x.options.brushMode ) {
    // reset the brush with None
    this.parcoords.brushMode('None')
    this.parcoords.brushMode(this.x.options.brushMode);
    this.parcoords.brushPredicate(this.x.options.brushPredicate);
  }

}
" 
    )
  )
)

@anobel
Copy link
Author

anobel commented Jan 8, 2016

@timelyportfolio thanks so much! I'm enjoying using this and iterating back and forth, no need to apologize!

The latest fix works; the axis are sorted correctly, the labels are correct, and brushing/interactivity work. When I set rownames=F, however, I subsequently lose the value that was fed into input$parcoords_brushed_row_names, which breaks some calculations.

I did not see any errors in the console with either T/F setting for row names.

@timelyportfolio
Copy link
Owner

that's probably why I chose the hideAxes route in the first place. Thanks for the reminder. I'll try to dig deeper to understand what is causing this problem. Until we get this worked out, if you just want to remove the labels and weren't concerned about the axis, you could set ticks = 0.

@timelyportfolio
Copy link
Owner

@anobel, if you could reinstall one more time to see if moving the hideAxis after the option handler works on your machine, I would really appreciate it. The code I tested is:

parcoords(
  mtcars
  ,rownames = F
  ,brushMode = "1d-multi"
  ,brushPredicate = "OR"
  ,dimensions = list(
    cyl = list(
      title = "cylinder",
      tickValues = unique(mtcars$cyl)
    )
  )
)

This should also still allow the rownames to be passed through with Shiny.

@anobel
Copy link
Author

anobel commented Jan 8, 2016

Success! works great!!

@timelyportfolio
Copy link
Owner

@anobel, very glad to hear! Let me know if you find anything else. I'll push to master and bump the version if we don't find anything over the next couple of days.

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

No branches or pull requests

2 participants