helgasoft / echarty

Minimal R/Shiny Interface to ECharts.js
https://helgasoft.github.io/echarty/
87 stars 3 forks source link

Mute visualMap color on specific series elements #22

Closed yogat3ch closed 1 year ago

yogat3ch commented 1 year ago

Hi @helgasoft, Hoping you have a tip for how to handle this situation where we want to have specific series elements use their default color and not the visualMap color. It appears possible based on these extraordinarily vague comments in the example under the Configure Mapping heading in the visualMap options

This stackoverflow shows a similar situation and how to apply visualMap: false to the data object in each series object to mute the visualMap.

However, in the old parallel example below the series objects refer to the datasetIndex instead of the data directly. image Setting an empty data object with just visualMap = FALSE overrides the datasetIndex as the data source and produces a chart with no series.

Any guidance on how to turn off visualMap for specific indices where the datasetIndex is used instead of data? I've added a browser statement to the reprex at the location where it most makes sense to see the echart and apply this modification.

With appreciation for any assistance you are able to offer

Parallel Reprex ```r library(shiny); library(echarty) ec.visualMap <- function(ec, .data, type = 'continuous', calculable = TRUE, inRange = list(color = c('deepskyblue', 'pink', 'pink', 'red')), min = NULL, max = NULL, dimension = 1, top = "middle", textGap = 5, padding = 2, itemHeight = 390, ...) { if (ncol(.data)) { .dimension <- ec.col_locate(dimension, .data) .min <- min %||% min(.data[[.dimension]], na.rm = TRUE) .max <- max %||% max(.data[[.dimension]], na.rm = TRUE) mods <- list( type = type, calculable = calculable, inRange = inRange, min = .min, max = .max, dimension = ec.dim(.dimension, .data), top = top, textGap = textGap, padding = padding, itemHeight = itemHeight, ... ) |> purrr::compact() ec$x$opts$visualMap <- mods } return(ec) } `%||%` <- rlang::`%||%` #' Convert R data dimension into JS dimension #' #' @param dim \code{chr/dbl} Column name or index #' @param ec \code{echarty} #' #' @return \code{dbl} #' @export ec.dim <- function(dim, ec) { UseMethod("ec.dim") } ec.data_extract <- function(ec) { ec$x$opts$dataset[[1]]$source[-1] |> purrr::map(unlist) |> as.data.frame.list() |> t() |> as.data.frame() |> tibble::remove_rownames() |> rlang::set_names(ec$x$opts$dataset[[1]]$source[[1]]) } ec.col_locate <- function(x, .data) { which(names(.data) == x) } #' @export ec.dim.character <- function(x, ec) { ec.col_locate(x, ec) - 1 } #' @export ec.dim.numeric <- function(x, ec) { x - 1 } #' @export ec.dim.default <- function(x, ec) { x } devtools::load_all() jsfn <- "() => { chart = get_e_charts('chart'); serie = chart.getModel().getSeries()[0]; indices = serie.getRawIndicesByActiveState('active'); Shiny.setInputValue('axisbrush', indices); };" ui <- fluidPage( ecs.output('chart'), DT::DTOutput('highlighted'), selectizeInput(inputId = "colormap", label = "Map color to ", choices = names(mtcars), selected = names(mtcars)[1] ), fluidRow(id = "alerts")) server <- function(input, output) { key <- NULL isolate({ .data <- mtcars |> head(8) |> tibble::rownames_to_column("ID") |> # tibble::remove_rownames() |> # does not affect layout dplyr::relocate(ID, .after = dplyr::last_col()) |> # move grouping column last dplyr::group_by(ID) key <- .data$ID # ct <- crosstalk::SharedData$new(reactiveVal(.data), key = "ID") # t_data <- crosstalk::SharedData$new(reactiveVal(.data), key = "ID") ct <- CT_data() t_data <- CT_data() }) ids <- c() # keep track of highlighted lines output$chart <- ecs.render({ ct$data_group() p <- ct |> echarty::ec.init(ctype = "parallel") |> ec.theme('dark-mushroom') p$x$opts$visualMap <- list(type= 'continuous', calculable= TRUE, inRange= list(color= c('deepskyblue','pink','red')), min= min(mtcars$mpg), max= max(mtcars$mpg), dimension= 0 # mpg is first column, index 0 in JS ) p$x$on <- list(list(event= 'axisareaselected', handler= htmlwidgets::JS(jsfn) )) s_order <- purrr::map_chr(p$x$opts$series, ~purrr::pluck(.x, "name")) .color = RColorBrewer::brewer.pal(length(s_order), "Set3") p$x$opts$color = .color p$x$opts$series <- purrr::map( p$x$opts$series, ~purrr::list_modify(.x, !!!rlang::list2(emphasis = list( disabled = FALSE, lineStyle = list(opacity = 1, width = 3, visualMap = FALSE) ))) ) p$x$opts$legend <- purrr::list_modify(p$x$opts$legend, !!!list( type = "scroll", orient = "horizontal", bottom = 0, top = NULL, icon = "pin", itemGap = 5, imageWidth = 10, imageHeight = 10 )) browser() # This produces an empty plot # p$x$opts$series <- purrr::imap(p$x$opts$series, \(.x,.y) { # purrr::list_modify(.x, data = list(visualMap = FALSE)) # }) p }) # Renders the DT with individually clicked rows from parallel plot highlighted with color output$highlighted <- DT::renderDT({ req(t_data) # .data <- t_data$full_data() # .rows <- 1:nrow(.data) # .colors <- t_data$tracking_cols()$source_col # if (shiny::isTruthy(highlighted())) { # browser() # # l <- length(highlighted()) # .colors <- grDevices::colorRampPalette(do.call(c, color_theme[paste0("scenario_", 1:2)]))(l) # .rows <- 1:l # .data <- .data[c(highlighted() + 1, setdiff(1:nrow(.data), highlighted() + 1)), ] # } dt <- DT::datatable( t_data, selection = list(mode = "multiple", target = "row"), #style = "bootstrap4", filter = list(position = "top"), escape = FALSE, callback = DT::JS(c( "table.on('draw.dt', function(e, datatable){", glue::glue("Shiny.setInputValue('highlighted' + '_page', table.page() + 1);"), "});" # UU::glue_js("if (table.page() != *{(input$policy_chooser_page %||% 1) - 1}*) {"), # paste0('setTimeout(function() {table.page(', (input$policy_chooser_page %||% 1) - 1,').draw(false);}, 100);'), # "};" )), extensions = "KeyTable", options = list( keys = TRUE, search = list(regex = TRUE), columnDefs = list( list(orderSequence = c("desc", "asc"), targets = "_all"), list(className = "dt-center", targets = "_all") ), processing = FALSE, pageLength = 10, lengthMenu = list(c(5, 10, 25, 50, -1), c("5", "10", "25", "50", "All")) ) ) dt }, server = FALSE) observeEvent(input$axisbrush, { print(input$axisbrush) }) history <- shinyVirga::rv( chart = NULL, table = NULL ) dispatch_action <- reactiveVal() observeEvent(input$chart_click, { .s <- input$chart_click$seriesName if (.s %in% history$chart) { dispatch_action(list(type= 'downplay', seriesName = .s)) history$chart <- setdiff(history$chart, .s) } else { dispatch_action(list(type= 'highlight', seriesName = .s)) history$chart <- c(history$chart, .s) } DT::dataTableProxy("highlighted") |> DT::selectRows(which(key %in% history$chart)) history$table <- intersect(key, history$chart) #virgaUtils::dbg_msg("chart_click: {.val {paste0(history$table, collapse = ',')}}") }, priority = 1) observeEvent(sum(t_data$selection()), { .selected <- key[t_data$selection()] if (length(.selected) < length(history$chart)) { now_selected <- setdiff(history$chart, .selected) dispatch_action(list(type = "downplay", seriesName = now_selected)) history$chart <- intersect(history$chart, .selected) } else if (length(.selected) > length(history$chart)) { dispatch_action(list(type = "highlight", seriesName = setdiff(.selected, history$chart))) history$table <- history$chart <- intersect(.selected, history$chart) } #virgaUtils::dbg_msg("table_click: {.val {paste0(history$table, collapse = ',')}}") # if the table is clicked, }, priority = 1) observeEvent(dispatch_action(), { # echarty built-in event #virgaUtils::dbg_msg("dispatch_action: {.val {dispatch_action()}}") # i <- which(t_data$data()[[key]] %in% highlighted()) # # Select the corresponding row in the table # DT::dataTableProxy("highlighted") |> # DT::selectRows(i) # # Update the selection in the table data crosstalk object # t_data$selection(i) p <- echarty::ecs.proxy('chart') p$x$opts <- dispatch_action() p |> echarty::ecs.exec('p_dispatch') }) observeEvent(input$colormap, { echarty::ecs.proxy("chart") |> ec.visualMap(dimension = input$colormap, .data = mtcars) |> echarty::ecs.exec("p_dispatch") }, ignoreInit = TRUE) } shinyApp(ui= ui, server= server) ```
sessionInfo() ``` R version 4.2.1 (2022-06-23) Platform: aarch64-apple-darwin20 (64-bit) Running under: macOS Ventura 13.1 Matrix products: default LAPACK: /Library/Frameworks/R.framework/Versions/4.2-arm64/Resources/lib/libRlapack.dylib locale: [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8 attached base packages: [1] stats graphics grDevices datasets utils methods base other attached packages: [1] dmdu_0.0.0.9001 testthat_3.1.6 echarty_1.4.5 shiny_1.7.4 loaded via a namespace (and not attached): [1] colorspace_2.0-3 ellipsis_0.3.2 rprojroot_2.0.3 snakecase_0.11.0 [5] markdown_1.1 fs_1.5.2 rstudioapi_0.14 roxygen2_7.2.1 [9] remotes_2.4.2 bit64_4.0.5 DT_0.23 golem_0.3.4 [13] fansi_1.0.3 lubridate_1.9.0 xml2_1.3.3 cachem_1.0.6 [17] knitr_1.40 shinytest2_0.2.0.9000 config_0.3.1 pkgload_1.3.0 [21] jsonlite_1.8.3 yardstick_1.1.0 tippy_1.0.0 readr_2.1.2 [25] compiler_4.2.1 BOR_0.4.0 httr_1.4.4 assertthat_0.2.1 [29] fastmap_1.1.0 lazyeval_0.2.2 cli_3.4.1 later_1.3.0 [33] htmltools_0.5.4 prettyunits_1.1.1 tools_4.2.1 gtable_0.3.1 [37] glue_1.6.2 dplyr_1.1.0 Rcpp_1.0.9 sortable_0.4.6 [41] jquerylib_0.1.4 vctrs_0.5.2 crosstalk_1.2.1 parsnip_1.0.2 [45] xfun_0.36 stringr_1.5.0 ps_1.7.1 brio_1.1.3 [49] rvest_1.0.3 english_1.2-6 timechange_0.1.1 UU_1.15.0 [53] mime_0.12 miniUI_0.1.1.1 lifecycle_1.0.3 shinyvalidate_0.1.2 [57] renv_0.16.0 devtools_2.4.5 scales_1.2.1 vroom_1.5.7 [61] hms_1.1.2 promises_1.2.0.1 parallel_4.2.1 RColorBrewer_1.1-3 [65] virgaUtils_0.4.0 curl_4.3.3 rpart.plot_3.1.1 yaml_2.3.5 [69] memoise_2.0.1 ggplot2_3.3.6 sass_0.4.5 rpart_4.1.16 [73] stringi_1.7.8 desc_1.4.2 RMySQL_0.10.23 cicerone_1.0.5.9000 [77] hardhat_1.2.0 pkgbuild_1.3.1 attempt_0.3.1 rlang_1.0.6 [81] pkgconfig_2.0.3 evaluate_0.16 purrr_1.0.1 patchwork_1.1.1 [85] htmlwidgets_1.5.4 bit_4.0.4 cowplot_1.1.1 processx_3.7.0 [89] tidyselect_1.2.0 OpenMCE_1.0.0 shinyVirga_0.15.0 plyr_1.8.7 [93] magrittr_2.0.3 learnr_0.10.1 R6_2.5.1 generics_0.1.3 [97] profvis_0.3.7 DBI_1.1.3 pillar_1.8.1 withr_2.5.0 [101] tibble_3.1.8 reactable_0.3.0 crayon_1.5.2 shinyWidgets_0.7.6 [105] bs4Dash_2.2.1 utf8_1.2.2 plotly_4.10.1 tzdb_0.3.0 [109] rmarkdown_2.20 crssDB_1.0.0 urlchecker_1.0.1 tidyrules_0.1.5 [113] usethis_2.1.6 grid_4.2.1 data.table_1.14.2 callr_3.7.2 [117] digest_0.6.30 xtable_1.8-4 tidyr_1.2.1 httpuv_1.6.6 [121] pool_0.1.6 munsell_0.5.0 viridisLite_0.4.1 bslib_0.4.0 [125] sessioninfo_1.2.2 shinyjs_2.1.0 ```
helgasoft commented 1 year ago

Here is the solution

library(echarty)
set.seed(222)
cns <- data.frame(   
  name = c('United States','China','Russia','Canada','Australia','Brazil'),
  visualMap = c(T,T,T,T,F,F),
  value = runif(6, 1, 100)   # last column = default for visualMap
)

# with series.data
ec.init(load= 'world',
  color= c('violet','green'),
  series= list(list(
    type='map', geoIndex=0, colorBy= 'data',
    data= ec.data(cns, 'names'))),
  visualMap= list(calculable=TRUE, max=100)
)

# with dataset in "array of classes" format
ec.init(load= 'world',
  # color= default palette
  dataset= list(
    dimensions= colnames(cns), source= ec.data(cns, 'names')),
  series= list(list(
    type='map', geoIndex=0, colorBy= 'data')),
  visualMap= list(calculable=TRUE, max=100)
)

image

yogat3ch commented 1 year ago

Thanks for the tip @helgasoft! I'm honestly having difficulty adapting the configuration of that map echart's dataset element, and how the visualMap false was applied to how the data is mapped out in our parallel plot which is more like the reprex above. We're committed to using crosstalk as we've already built a good portion of the functionality of our app around it's features, so I'm not sure how we can manually input the data to dataset -> source on the ec.init call (as below) while preserving the crosstalk functionality? Especially when the policy_id column is a grouping variable, and should not show up in the chart. I tried adapting the code below by pluggin in the data with ec.data(format = "names") but the grouping variable isn't respected and appears as a parallel axis in the plot.

ec.init(load= 'world',
  # color= default palette
  dataset= list(
    dimensions= colnames(cns), source= ec.data(cns, 'names')),

Our dataset option is composed of a 2D array of the entire dataset called source, and then an object with the transform element for each of the series in the parallel plot. I've copy pasted the list structure in JSON format below with many of the source entries removed. You can see where I attempted to add the visualMap: false to each element that seemed to correspond to the series, but that did not have the intended effect of turning off the visualMap. Do you know where the visualMap: false should go in this data structure?

This formatting of data in echarts seems to change with each chart type, and I find it baffling to try to understand where each option should go for each and every chart type when it's changing all the time! I hope you can understand and offer some assistance that's adapted to the parallel plot dataset format. It will be much appreciated!

[
  {
    "source": [
      ["Avg Combo Storage", "Lb Shrtg Frqncy", "Lb Shrtg Volume", "Lee Ferry Deficit", "Max Annual Lb Shrtg", "Mead 1000", "Powell 3490", "Powell Wy Release", "policy_id"],
      [
        -21403112.5,
        63.125,
        148589.375,
        0.9375,
        287500,
        16.4844,
        5.9896,
        9344095,
        "MOEA1"
      ],
      [
        -20867988.75,
        49.375,
        97779.9525,
        0,
        100000,
        17.1354,
        9.0885,
        9402548.75,
        "MOEA2"
      ],
      [
        -21896500,
        38.75,
        199210.5025,
        0,
        1081250,
        6.5885,
        7.4219,
        9371616.25,
        "MOEA3"
      ]
    ]
  },
  {
    "transform": {
      "type": "filter",
      "config": {
        "dimension": "policy_id",
        "=": "MOEA1"
      }
    },
    "visualMap": false
  },
  {
    "transform": {
      "type": "filter",
      "config": {
        "dimension": "policy_id",
        "=": "MOEA10"
      }
    },
    "visualMap": false
  }
]
yogat3ch commented 1 year ago

Just dropped some in the hat for you

image
helgasoft commented 1 year ago

To sum it up: (1) series.data and (2) dataset in "array of classes" format (key-value format) cannot be used because crosstalk needs a straight data.frame which translates to straight dataset in JS. Currently such dataset cannot accommodate the visualMap data attribute. Wish there was a parameter series.encode.visualMap: <column name/index> to bring that functionality.

The only remaining means of overwriting color would be thru action highlight. Like so:

library(echarty)
p <- mtcars |> ec.init( js= 'window.idxs = [3,10];',
  parallelAxis= ec.paxis(mtcars, cols= c('gear','cyl','carb','hp')),
  series= list(list(
    type='parallel', smooth=TRUE, lineStyle= list(width= 4)
    ,emphasis= list(lineStyle= list(color= 'blue', opacity= 1))
  )),
  visualMap= list(calculable= TRUE, max= 100)
)
p$x$on <- list(
  list(event= 'finished', handler= htmlwidgets::JS("function (event) {
    if (window.idxs.length>0)
      this.dispatchAction({ type: 'highlight', dataIndex: window.idxs }); 
    }")
  )
)
p

image

yogat3ch commented 1 year ago

Hey @helgasoft, Thank you for the helpful follow-up!

cannot be used because crosstalk needs a straight data.frame which translates to straight dataset in JS

Ah this is helpful context to have about the limitations of using crosstalk

Currently such dataset cannot accommodate the visualMap data attribute

Doh, that's too bad.

Wish there was a parameter series.encode.visualMap: <column name/index> to bring that functionality.

That would certainly be nice. Should we make a feature request on echarts about this?

The only remaining means of overwriting color would be thru action highlight.

Oh, theres an idea 💡 I think I have a sense for how we might accomplish that in our use case as we already have highlight in use.

Thank you for the assistance!

yogat3ch commented 1 year ago

Success! Thank you!

image
helgasoft commented 1 year ago

After more pondering, it is possible to have dataset in key-value format and crosstalk working together. Trick is to update the main dataset after all presets have been completed, with ec.upd(). Below are two series examples (parallel and scatter). Both show three items with their own color, excluded from visualMap. So far, so good.

Code for v.1.4.7 ```r library(crosstalk); library(dplyr) library(echarty) # tested with v.1.4.7 tmp <- cars |> mutate(visualMap= c(rep(T,nrow(cars)-3),F,F,F)) dd <- list(dimensions= colnames(tmp), source= ec.data(tmp, 'names')) tmp <- SharedData$new(tmp) p <- ec.init(tmp, color= c('#444','green','blue'), series= list(list( # type parallel has no 'select', just 'highlight' # won't respond to slider selection: one reason to rewrite crosstalk in v.1.5+ type='parallel', smooth=TRUE, colorBy= 'data', lineStyle= list(width= 4, Xopacity= 0.9) ,emphasis= list(lineStyle= list(color= 'magenta', opacity= 1)) # type scatter has 'select', will respond to slider selection # type='scatter', colorBy= 'data', symbolSize= 15, # selectedMode='multiple', # select= list(itemStyle= list(color= 'magenta', opacity= 1)) )) ,visualMap= list(calculable= TRUE, max= max(cars$speed), dimension= 'speed') ) |> ec.upd({ dataset[[1]] <- dd }) htmltools::browsable( htmltools::tagList( filter_slider("speed", "speed", tmp, ~speed, width= "70%"), p )) ```


However, parallel example shows a crosstalk problem in echarty v.1.4.7. Since parallel series do not have a selection attribute, slider selection will not show in the chart. The scatter example is fine because scatter has selection attribute.

This is one of the reasons to rewrite crosstalk in echarty v.1.5.0. Yes, there is a downside of adding a dataset ('Xtalk') and a column('XkeyX'), but crosstalk now works correctly.
Next version 1.5.3 will have an option to skip XkeyX if df data.frame has a unique ID column, see SharedData$new(df, key).