R/Plotter.R

Defines functions .jn_scatter .johnson_neyman cbands

Plotter <- R6::R6Class(
    "Plotter",
    cloneable = FALSE,
    class = FALSE,
    inherit = Scaffold,
    public = list(
        options = NULL,
        scatterDodge = NULL,
        scatterClabel = NULL,
        scatterY = NULL,
        scatterX = NULL,
        scatterXscale = FALSE,
        x_range = list(),
        scatterZ = NULL,
        scatterCluster = NULL,
        scatterModerators = NULL,
        scatterBars = FALSE,
        scatterRaw = FALSE,
        scatterType = NULL,
        largedata = NULL,
        initialize = function(jmvobj, operator) {
            super$initialize(jmvobj)
            private$.results <- jmvobj$results
            private$.operator <- operator
            private$.datamatic <- operator$datamatic
            self$scatterRaw <- self$options$plot_raw
            if (self$option("plot_scale")) {
                self$scatterType <- self$options$plot_scale
            } else {
                self$scatterType <- "response"
            }
        },
        ## plot should be inited and prepared storing all information needed to produce the plot in
        ## the image (Image object) state. It does not matter if the state is set in init() or run()
        ## as long as the plot does not require the model to be re-estimated
        ## Info present at .init() (number of variables, variables names, etc) me be omitted from the
        ## information contained in the image$state
        initPlots = function() {
            private$.initMainPlot()
            private$.initJnPlot()
            private$.initClusterPlots()
            private$.initRandHist()
        },
        preparePlots = function(image, ggtheme, theme, ...) {
            ## here are the plots that require some preparations. All plots
            ## are  prepared
            if (isFALSE(private$.operator$ok)) {
                return()
            }

            private$.prepareMainPlot()
            private$.prepareQqplot()
            private$.prepareNormplot()
            private$.prepareResidplot()
            private$.prepareClusterBoxplot()
            private$.prepareClusterResPred()
            private$.prepareClusterResPredGrid()
            private$.prepareRandHist()
            private$.prepareJnPlot()
        },
        scatterPlot = function(image, ggtheme, theme) {
            ## debug: return this to see what error is in the plotter code ###
            if (!is.something(image$state$plotData)) {
                # pp<-ggplot2::ggplot(data.frame(1:3))+ggplot2::ggtitle(image$key)
                # return(pp)
                return()
            }

            ## collect the data
            data <- image$state$plotData

            linesdiff <- (theme$bw || self$options$plot_black)
            ### prepare aestetics for one or two way scatterplot
            if (is.null(self$scatterZ)) {
                names(data)[1] <- "x"
                .aestetics <- ggplot2::aes(x = x, y = estimate, group = 1)
                .aesbar <- ggplot2::aes(x = x, ymin = lower, ymax = upper)
            } else {
                names(data)[1:2] <- c("x", "z")
                if (isFALSE(linesdiff)) {
                    .aestetics <- ggplot2::aes(x = x, y = estimate, group = z, colour = z)
                    .aesbar <- ggplot2::aes(x = x, ymin = lower, ymax = upper, group = z, color = z, fill = z)
                } else {
                    .aestetics <- ggplot2::aes(x = x, y = estimate, group = z, linetype = z)
                    .aesbar <- ggplot2::aes(x = x, ymin = lower, ymax = upper, group = z, linetype = z)
                    if (!theme$bw && self$options$plot_black) {
                        .aestetics <- ggplot2::aes(x = x, y = estimate, group = z, linetype = z, colour = z)
                    }
                }
            }

            ## initialize plot
            p <- ggplot2::ggplot()

            # give a scale to the Y axis

            if (is.number(image$state$y_range$ticks)) {
                if (self$option("plot_y_ticks_exact")) {
                    if (any(sapply(image$state$y_range["min","max","ticks"], is.number))) {
                        self$warning <- list(
                            topic = "plotnotes",
                            message = paste("Exact ticking requires to set min and max"),
                            head = "warning"
                        )
                        p <- p + ggplot2::scale_y_continuous(limits = as.numeric(image$state$y_range))
                    } else {
                        p <- p + ggplot2::scale_y_continuous(limits = as.numeric(image$state$y_range), breaks = seq(image$state$y_range$min, image$state$y_range$max, length.out = image$state$y_range$ticks))
                    }
                } else {
                    p <- p + ggplot2::scale_y_continuous(limits = as.numeric(image$state$y_range), n.breaks = image$state$y_range$ticks)
                }
            } else {
                p <- p + ggplot2::scale_y_continuous(limits = as.numeric(image$state$y_range))
            }




            #### plot the actual data if required

            if (self$scatterRaw) {
                rawdata <- image$state$rawData
                y <- self$scatterY$name64
                x <- self$scatterX$name64
                z <- self$scatterZ$name64
                #
                .aesraw <- ggplot2::aes(x = .data[[x]], y = .data[[y]])

                if (!is.null(self$scatterZ)) {
                    if (self$scatterZ$type == "factor") {
                        .aesraw <- ggplot2::aes(x = .data[[x]], y = .data[[y]], color = .data[[z]])
                    }
                }

                p <- p + ggplot2::geom_point(
                    data = rawdata,
                    .aesraw,
                    show.legend = FALSE, alpha = 0.5, shape = 16
                )
            }


            ##### END OF RAW DATA #############

            if (is.something(image$state$randomData)) {
                randomData <- image$state$randomData
                if ("z" %in% names(randomData)) {
                    .aesrandom <- ggplot2::aes(x = x, y = y, group = cluster, colour = z)
                    p <- p + ggplot2::geom_line(
                        data = randomData,
                        .aesrandom,
                        linewidth = 0.5,
                        alpha = .80
                    )
                } else {
                    .aesrandom <- ggplot2::aes(x = x, y = y, group = cluster)
                    p <- p + ggplot2::geom_line(
                        data = randomData,
                        .aesrandom,
                        color = "gray50",
                        linewidth = 0.5,
                        alpha = .80
                    )
                    if (attr(randomData, "xbetween")) {
                        p <- p + ggplot2::geom_point(
                            data = randomData,
                            .aesrandom,
                            color = "gray4",
                            linewidth = 0.5,
                            alpha = .80
                        )
                    }
                }
            }

            ######### fix the bars ##########
            if (self$scatterBars) {
                if (self$scatterX$type == "factor") {
                    p <- p + ggplot2::geom_errorbar(data = data, .aesbar, linewidth = .9, width = .3, position = self$scatterDodge, show.legend = FALSE)
                } else {
                    p <- p + ggplot2::geom_ribbon(data = data, .aesbar, linetype = 0, show.legend = F, alpha = 0.2)
                }
            }
            #########  ##########


            ### plot the lines
            p <- p + ggplot2::geom_line(
                data = data,
                .aestetics,
                linewidth = 1.2,
                position = self$scatterDodge
            )


            ### plot the points for factors
            if (self$scatterX$type == "factor") {
                p <- p + ggplot2::geom_point(
                    data = data,
                    .aestetics,
                    shape = 21, size = 4, fill = "white",
                    position = self$scatterDodge, show.legend = FALSE
                )
            } else {
                # give a scale to the Z axis
                p <- p + ggplot2::scale_x_continuous(limits = as.numeric(image$state$x_range))

                if (is.number(image$state$x_range$ticks)) {
                    if (self$option("plot_x_ticks_exact")) {
                        if (any(sapply(image$state$x_range["min","max","ticks"], is.number))) {
                            self$warning <- list(
                                topic = "plotnotes",
                                message = paste("Exact ticking for the X-axis requires to set min and max"),
                                head = "warning"
                            )
                        } else {
                            p <- p + ggplot2::scale_x_continuous(limits = as.numeric(image$state$x_range), breaks = seq(image$state$x_range$min, image$state$x_range$max, length.out = image$state$x_range$ticks))
                        }
                    } else {
                        p <- p + ggplot2::scale_x_continuous(limits = as.numeric(image$state$x_range), n.breaks = image$state$x_range$ticks)
                    }
                }
            }
            ### end of dealing with scales

            p <- p + ggtheme

            p <- p + ggplot2::labs(
                x = self$scatterX$name, y = self$scatterY$name,
                colour = self$scatterClabel,
                shape = self$scatterClabel,
                linetype = self$scatterClabel
            )

            if (self$options$plot_black) {
                p <- p + ggplot2::theme(legend.key.width = ggplot2::unit(2, "cm"))
            }

            if (image$state$y_range$noticks) {
                p <- p + ggplot2::theme(axis.ticks.y = ggplot2::element_blank())
                p <- p + ggplot2::theme(axis.text.y = ggplot2::element_blank())
            }
            if (image$state$x_range$noticks) {
                p <- p + ggplot2::theme(axis.ticks.x = ggplot2::element_blank())
                p <- p + ggplot2::theme(axis.text.x = ggplot2::element_blank())
            }


            return(p)
        },
        jnPlot = function(image, ggtheme, theme) {
            if (is.null(image$state)) {
                return()
            }


            datalist <- image$state
            alpha <- .05
            pmsg <- paste("p <", alpha)
            colors <- ggtheme[[2]]$palette(2)
            sig.color <- colors[2]
            insig.color <- colors[1]

            ypos <- min(c(datalist$cbso1$Lower, datalist$cbso2$Lower, datalist$cbsi$Lower), na.rm = T)

            p <- ggplot2::ggplot()
            suppressMessages(p <- p + ggtheme)


            p <- p + ggplot2::geom_path(data = datalist$cbso1, ggplot2::aes(x = z, y = slopes, color = Significance), size = .8, show.legend = FALSE)
            p <- p + ggplot2::geom_path(data = datalist$cbsi, ggplot2::aes(x = z, y = slopes, color = Significance), size = .8, show.legend = FALSE)
            p <- p + ggplot2::geom_path(data = datalist$cbso2, ggplot2::aes(x = z, y = slopes, color = Significance), size = .8, show.legend = FALSE)
            p <- p + ggplot2::geom_ribbon(data = datalist$cbso1, ggplot2::aes(x = z, ymin = Lower, ymax = Upper, fill = Significance), alpha = 0.2)
            p <- p + ggplot2::geom_ribbon(data = datalist$cbsi, ggplot2::aes(x = z, ymin = Lower, ymax = Upper, fill = Significance), alpha = 0.2)
            p <- p + ggplot2::geom_ribbon(data = datalist$cbso2, ggplot2::aes(x = z, ymin = Lower, ymax = Upper, fill = Significance), alpha = 0.2)
            p <- p + ggplot2::scale_fill_manual(
                values = c(Significant = sig.color, Insignificant = insig.color), labels = c("Significant (p < .05)", " n.s (p \u2265 .05)"),
                breaks = c("Significant", "Insignificant"), drop = FALSE, guide = ggplot2::guide_legend(order = 2)
            )
            p <- p + ggplot2::scale_color_manual(values = c(Significant = sig.color, Insignificant = insig.color))

            p <- p + ggplot2::geom_hline(ggplot2::aes(yintercept = datalist$intercept))
            p <- p + ggplot2::geom_segment(
                ggplot2::aes(
                    x = datalist$modrange[1],
                    xend = datalist$modrange[2], y = datalist$intercept, yend = datalist$intercept, linetype = "Range of\nobserved\ndata"
                ),
                lineend = "square", size = 1.25
            )
            p <- p + ggplot2::scale_linetype_discrete(name = " ", guide = ggplot2::guide_legend(order = 1))

            if (datalist$bounds[1] < datalist$modrange[1]) {
            } else if (datalist$all_sig == FALSE) {
                p <- p + ggplot2::geom_vline(ggplot2::aes(xintercept = datalist$bounds[1]), linetype = 2, color = sig.color)
                p <- p + ggplot2::geom_text(ggplot2::aes(label = round(datalist$bounds[1], digits = 3), x = datalist$bounds[1], y = ypos), color = "black")
            }
            if (datalist$bounds[2] > datalist$modrange[2]) {
            } else if (datalist$all_sig == FALSE) {
                p <- p + ggplot2::geom_vline(ggplot2::aes(xintercept = datalist$bounds[2]), linetype = 2, color = sig.color)
                p <- p + ggplot2::geom_text(ggplot2::aes(label = round(datalist$bounds[2], digits = 3), x = datalist$bounds[2], y = ypos), color = "black")
            }
            p <- p + ggplot2::xlim(datalist$modrange)
            p <- p + ggplot2::theme(legend.key.size = ggplot2::unit(1, "lines"))
            p <- p + ggplot2::ylab(paste("Slope of ", self$scatterX$name))
            p <- p + ggplot2::xlab(self$scatterZ$name)
            #  suppressMessages(p <- p + ggtheme)
            p <- p + ggplot2::labs(fill = NULL) + ggplot2::guides(colour = "none")
            return(p)
        },
        qqplot = function(image, theme, ggtheme) {
            if (!self$option("qq_plot")) {
                return()
            }

            if (!is.something(image$state$residuals)) {
                return()
            }

            residuals <- image$state$residuals
            #              residuals <- as.numeric(scale(stats::residuals(private$.operator$model)))
            df <- as.data.frame(qqnorm(residuals, plot.it = FALSE))
            plot <- ggplot2::ggplot(data = df, ggplot2::aes(y = y, x = x)) +
                ggplot2::geom_abline(slope = 1, intercept = 0, colour = theme$color[1]) +
                ggplot2::geom_point(ggplot2::aes(x = x, y = y), linewidth = 2, colour = theme$color[1]) +
                ggplot2::xlab("Theoretical Quantiles") +
                ggplot2::ylab("Standardized Residuals")

            plot + ggtheme
        },
        normplot = function(image, theme, ggtheme) {
            if (!self$option("norm_plot")) {
                return()
            }

            if (!is.something(image$state$residuals)) {
                return()
            }

            fill <- theme$fill[2]
            color <- theme$color[1]
            data <- image$state$residuals
            names(data) <- "x"
            # library(ggplot2)
            plot <- ggplot2::ggplot(data = data, ggplot2::aes(x = x)) +
                ggplot2::labs(x = "Residuals", y = "density")
            ## after_stat() is new, so we used to wait for jamovi to update. In the meantime, we used ..density..
            ## now after_stat() works
            plot <- plot + ggplot2::geom_histogram(ggplot2::aes(y = ggplot2::after_stat(density)), position = "identity", stat = "bin", color = color, fill = fill)
            #       plot <- plot + ggplot2::geom_histogram(ggplot2::aes(y = "..density.."), position = "identity", stat = "bin", color = color, fill = fill)
            plot <- plot + ggplot2::stat_function(fun = stats::dnorm, args = list(mean = mean(data$x), sd = stats::sd(data$x)))

            themeSpec <- ggplot2::theme(axis.text.y = ggplot2::element_blank(), axis.ticks.y = ggplot2::element_blank())
            plot <- plot + ggtheme + themeSpec

            return(plot)
        },
        residPlot = function(image, theme, ggtheme) {
            if (!self$option("resid_plot")) {
                return()
            }

            if (!is.something(image$state$data)) {
                return()
            }


            fill <- theme$fill[2]
            color <- theme$color[1]
            data <- image$state$data
            size <- 2

            # library(ggplot2)
            plot <- ggplot2::ggplot(data = data, ggplot2::aes(x = predicted, y = residuals)) +
                ggplot2::labs(x = "Predicted", y = "Residuals")
            plot <- plot + ggplot2::geom_point(shape = 21, color = color, fill = fill, size = size)

            if (self$option("plot_extremes")) {
                cutoff1 <- quantile(data$residuals, .01)
                cutoff2 <- quantile(data$residuals, .99)
                edata <- data[data$residuals <= cutoff1 | data$residuals >= cutoff2, ]
                plot <- plot + ggplot2::geom_label(
                    data = edata, ggplot2::aes(x = predicted, y = residuals, label = rownames(edata)),
                    show.legend = FALSE,
                    position = ggplot2::position_jitter()
                )
            }
            plot <- plot + ggplot2::geom_hline(yintercept = 0, colour = "gray")
            plot <- plot + ggtheme
            return(plot)
        },
        clusterBoxplot = function(image, ggtheme, theme) {
            ########## working here ##########

            if (!self$option("cluster_boxplot")) {
                return()
            }

            if (!is.something(image$state)) {
                return(FALSE)
            }

            cluster <- image$state$cluster
            data <- image$state$data

            data$cluster <- data[[cluster]]
            plot <- ggplot2::ggplot(data, ggplot2::aes(cluster, .resid)) +
                ggplot2::geom_boxplot()

            if (self$option("plot_extremes")) {
                cutoff1 <- quantile(data$.resid, .01)
                cutoff2 <- quantile(data$.resid, .99)
                edata <- data[data$.resid <= cutoff1 | data$.resid >= cutoff2, ]
                plot <- plot + ggplot2::geom_label(
                    data = edata, ggplot2::aes(x = cluster, y = .resid, label = rownames(edata)),
                    show.legend = FALSE,
                    position = ggplot2::position_jitter()
                )
            }
            plot <- plot + ggplot2::coord_flip()
            plot <- plot + ggplot2::xlab(fromb64(cluster)) + ggplot2::ylab("Residuals")
            plot <- plot + ggtheme

            return(plot)
        },
        clusterResPred = function(image, ggtheme, theme) {
            ########## working here ##########

            if (!self$option("cluster_respred")) {
                return()
            }

            if (!is.something(image$state)) {
                return(FALSE)
            }

            cluster <- image$state$cluster
            data <- image$state$data

            data$cluster <- data[[cluster]]
            plot <- ggplot2::ggplot(data = data, ggplot2::aes(x = .fitted, y = .resid, color = cluster))
            plot <- plot + ggplot2::labs(x = "Predicted", y = "Residuals", color = fromb64(cluster))
            plot <- plot + ggplot2::geom_point(shape = 19, size = 2)
            plot <- plot + ggplot2::geom_hline(yintercept = 0, colour = "gray")

            if (self$option("plot_extremes")) {
                cutoff1 <- quantile(data$.resid, .01)
                cutoff2 <- quantile(data$.resid, .99)
                edata <- data[data$.resid <= cutoff1 | data$.resid >= cutoff2, ]
                plot <- plot + ggplot2::geom_label(
                    data = edata, ggplot2::aes(x = .fitted, y = .resid, label = rownames(edata)),
                    show.legend = FALSE,
                    position = ggplot2::position_jitter()
                )
            }

            plot <- plot + ggtheme
            plot <- plot + ggplot2::theme(legend.position = "bottom")
            return(plot)
        },
        clusterResPredGrid = function(image, ggtheme, theme) {
            if (!self$option("cluster_respred_grid")) {
                return()
            }

            if (!is.something(image$state)) {
                return(FALSE)
            }

            cluster <- image$state$cluster
            data <- image$state$data
            data$cluster <- data[[cluster]]
            plot <- ggplot2::ggplot(data = data, ggplot2::aes(x = .fitted, y = .resid))
            plot <- plot + ggplot2::labs(x = "Predicted", y = "Residuals")
            plot <- plot + ggplot2::geom_point(shape = 19)
            plot <- plot + ggplot2::geom_hline(yintercept = 0, colour = "gray")

            if (self$option("plot_extremes")) {
                cutoff1 <- quantile(data$.resid, .01)
                cutoff2 <- quantile(data$.resid, .99)
                edata <- data[data$.resid <= cutoff1 | data$.resid >= cutoff2, ]
                plot <- plot + ggplot2::geom_label(
                    data = edata, ggplot2::aes(x = .fitted, y = .resid, label = rownames(edata)),
                    show.legend = FALSE,
                    position = ggplot2::position_jitter()
                )
            }

            plot <- plot + ggtheme
            plot <- plot + ggplot2::theme(legend.position = "bottom")
            plot <- plot + ggplot2::facet_wrap(cluster)
            return(plot)
        },
        randHist = function(image, ggtheme, theme) {
            if (!self$option("rand_hist")) {
                return()
            }
            if (!is.something(image$state)) {
                return()
            }

            data <- image$state$data
            fill <- theme$fill[2]
            color <- theme$color[1]
            alpha <- 0.4
            plot <- ggplot2::ggplot(data = data, ggplot2::aes(x = x)) +
                ggplot2::labs(x = "Coefficients", y = "density")

            plot <- plot + ggplot2::geom_histogram(ggplot2::aes(y = ..density..),
                position = "identity",
                stat = "bin", color = color, fill = fill
            )
            plot <- plot + ggplot2::stat_function(fun = dnorm, args = list(mean = mean(data$x), sd = sd(data$x)))

            themeSpec <- ggplot2::theme(
                axis.text.y = ggplot2::element_blank(),
                axis.ticks.y = ggplot2::element_blank()
            )
            plot <- plot + ggtheme + themeSpec

            return(plot)
        }
    ), # end of public
    private = list(
        .datamatic = FALSE,
        .results = NULL,
        .operator = NULL,
        .initMainPlot = function() {
            if (!is.something(self$options$plot_x)) {
                return()
            }

            jinfo("PLOTTER: init main plot")

            resultsgroup <- private$.results$get("mainPlots")
            y <- self$options$dep
            x <- self$options$plot_x
            z <- self$options$plot_z
            moderators <- self$options$plot_by
            if (self$options$model_type == "multinomial") {
                moderators <- c(z, moderators)
                z <- self$options$dep
                self$scatterType <- "response"
            }

            if (self$options$model_type == "ordinal" & self$scatterType != "mean.class") {
                moderators <- c(z, moderators)
                z <- self$options$dep
            }

            dims <- unlist(lapply(moderators, function(mod) private$.datamatic$variables[[tob64(mod)]]$nlevels))

            if (!is.something(dims)) {
                dims <- 1
            }

            n <- prod(dims)
            for (i in seq_len(n)) {
                resultsgroup$addItem(key = i)
            }

            ### some options here
            self$scatterX <- private$.datamatic$variables[[tob64(x)]]
            self$scatterY <- private$.datamatic$variables[[tob64(y)]]
            self$scatterModerators <- moderators

            if (is.something(z)) {
                self$scatterZ <- private$.datamatic$variables[[tob64(z)]]
            }


            if (self$options$plot_around != "none") {
                self$scatterBars <- TRUE
                self$scatterDodge <- ggplot2::position_dodge(0.2)
                self$scatterClabel <- paste(self$scatterZ$name, paste0("(", toupper(self$options$plot_around), ")"), sep = "\n")
            } else {
                self$scatterDodge <- ggplot2::position_dodge(0)
                self$scatterClabel <- self$scatterZ$name
            }
        },
        .prepareMainPlot = function() {
            if (!is.something(self$options$plot_x)) {
                return()
            }

            jinfo("PLOTTER: checking main plot")

            resultsgroup <- private$.results$get("mainPlots")
            ### stop if it is filled from previous run ###

            test <- any(unlist(sapply(resultsgroup$items, function(i) !i$isNotFilled())))
            if (test) {
                return()
            }

            jinfo("PLOTTER: prepare main plot")

            ### give a range to the x-axis, if needed
            x_range <- list(min = NA, max = NA, ticks = NA, noticks = FALSE)

            min <- as.numeric(self$optionValue("plot_x_min"))
            if (is.number(min)) {
                x_range$min <- min
            }

            max <- as.numeric(self$optionValue("plot_x_max"))
            if (is.number(max)) {
                x_range$max <- max
            }

            ticks <- as.numeric(self$optionValue("plot_x_ticks"))
            if (is.number(ticks)) {
                x_range$ticks <- ticks

                if (ticks < 0) {
                    self$warning <- list(topic = "plotnotes", message = "Invalid number of ticks (<0). The value is ignored.", type = "warning")
                    x_range$ticks <- NA
                }
                if (ticks == 1) {
                    self$warning <- list(topic = "plotnotes", message = "Invalid number of ticks (1). The value is ignored.", type = "warning")
                    x_range$ticks <- NA
                }

                if (ticks > 20) {
                    self$warning <- list(topic = "plotnotes", message = "Invalid number of ticks (>20). The value is ignored.", type = "warning")
                    x_range$ticks <- NA
                }
                if (ticks == 0) {
                    x_range$ticks <- NA
                    x_range$noticks <- TRUE
                }
            }


            self$x_range <- x_range
            ################



            moderators <- self$scatterModerators
            ### compute the expected values to be plotted ###
            data <- private$.estimate(self$scatterX$name, unlist(c(self$scatterZ$name, moderators)))

            ### prepare rawData if needed
            rawData <- mf.clean(mf.data(private$.operator$model))
            for (var in intersect(names(rawData), names(private$.datamatic$variables))) {
                varobj <- private$.datamatic$variables[[var]]
                if (varobj$type == "factor") {
                    levels(rawData[[var]]) <- varobj$levels_labels
                }
            }

            ### here we deal with plotting random effects, if needed
            randomData <- NULL

            if (self$option("plot_re") &&
                !self$option("model_type", "ordinal") &&
                !self$option("model_type", "multinomial")) {
                ### here we want to be sure that clusters passed as cluster1/cluster2 or cluster1:cluster2 works

                rawData <- private$.fix_clusters(rawData)

                .model <- private$.operator$model

                if (self$option("plot_re_method", "average")) {
                    formula <- private$.operator$formulaobj$keep(self$scatterX$name)
                    res <- try_hard(mf.update(private$.operator$model, formula = formula))
                    if (!isFALSE(res$error)) {
                        self$warning <- list(
                            topic = "plotnotes",
                            message = paste("Random effects cannot be plotted for", self$scatterX$name, " with the averaging method."),
                            head = "warning"
                        )
                    } else {
                        .model <- res$obj
                    }
                }
                y <- stats::predict(.model, type = self$scatterType)
                randomData <- as.data.frame(cbind(y, rawData))
                if (self$scatterX$type == "factor") {
                    levels(randomData[[self$scatterX$name64]]) <- self$scatterX$levels_labels
                }

                self$warning <- list(topic = "plotnotes", message = paste("Random effects are plotted across", self$scatterCluster$name), head = "info")
                # prepare a test for between variables to plot dots for random effects

                xbetween <- FALSE
                test <- tapply(as.numeric(rawData[[self$scatterX$name64]]), rawData[[self$scatterCluster$name64]], sd)
                test <- sum(sapply(test, function(x) as.numeric(is.na(x) || x == 0)))
                nc <- self$scatterCluster$nlevels
                if ((test / nc) > .30) xbetween <- TRUE
            }
            ### end of random ###


            ### deal with Y ####
            dep64 <- self$scatterY$name64

            ### give a range to the y-axis, if needed
            y_range <- list(min = NA, max = NA, ticks = NA, noticks = FALSE)

            if (self$option("plot_yscale")) {
                y_range$min <- self$scatterY$descriptive$min
                y_range$min <- self$scatterY$descriptive$max
            }

            min <- as.numeric(self$optionValue("plot_y_min"))

            if (is.number(min)) {
                y_range$min <- min
            }

            max <- as.numeric(self$optionValue("plot_y_max"))
            if (is.number(max)) {
                y_range$max <- max
            }

            ticks <- as.numeric(self$optionValue("plot_y_ticks"))

            if (is.number(ticks)) {
                y_range$ticks <- ticks

                if (ticks == 1) {
                    self$warning <- list(topic = "plotnotes", message = "Invalid number of ticks (1). The value is ignored.", head = "warning")
                    y_range$ticks <- NA
                }
                if (ticks < 0) {
                    self$warning <- list(topic = "plotnotes", message = "Invalid number of ticks (<0). The value is ignored.", head = "warning")
                    y_range$ticks <- NA
                }

                if (ticks > 20) {
                    self$warning <- list(topic = "plotnotes", message = "Invalid number of ticks (>20). The value is ignored.", head = "warning")
                    y_range$ticks <- NA
                }
                if (ticks == 0) {
                    y_range$ticks <- NA
                    y_range$noticks <- TRUE
                }
            }

            if (self$option("model_type", "multinomial")) {
                self$scatterRaw <- FALSE
            }

            if (self$scatterType == "link") {
                self$scatterRaw <- FALSE
                y_range$min <- NA
                y_range$max <- NA
                y_range$ticks <- NA
            }

            if (self$option("model_type", "multinomial")) {
                self$scatterRaw <- FALSE
            }

            if (self$scatterType == "link") {
                self$scatterRaw <- FALSE
                y_range$min <- NA
                y_range$max <- NA
                y_range$ticks <- NA
            }


            ### we need to be sure that the dependent variable is a continuous variable to plot the raw data ##

            if (self$scatterY$type == "factor") {
                levels(rawData[[dep64]]) <- 0:(self$scatterY$nlevels - 1)
                rawData[[dep64]] <- as.numeric(as.character(rawData[[dep64]]))
            }
            if (self$option("model_type", "ordinal")) {
                rawData[[dep64]] <- rawData[[dep64]] + 1
                if (self$option("plot_scale", "mean.class")) {
                    y_range$min   <- NA
                    y_range$max   <- NA
                    y_range$ticks <- NA
                }
            }
            if (self$option("model_type", c("logistic", "multinomial"))) {
                if (self$scatterType != "link") {
                    y_range$min <- NA
                }
                y_range$max   <- NA
                y_range$ticks <- NA
            }



            #### deal with rescaling
            if (self$scatterXscale) {
                if (self$scatterX$covs_scale == "clusterbasedcentered") {
                    self$warning <- list(
                        topic = "plotnotes",
                        message = "Rescaling cluster-wise centered variables may be misleading. Use `Covariates Scaling=None` if the original scale is necessary.",
                        head = "warning"
                    )
                }

                if (self$scatterX$covs_scale == "clusterbasedstandardized") {
                    self$warning <- list(
                        topic   = "plotnotes",
                        message = "Rescaling cluster-wise standardized variables may be misleading. Use `Covariates Scaling=None` if the original scale is necessary.",
                        head    = "warning"
                    )
                }

                data[[self$scatterX$name64]] <- private$.rescale(self$scatterX, data[[self$scatterX$name64]])

                if (is.something(rawData)) {
                    rawData[[self$scatterX$name64]] <- private$.rescale(self$scatterX, rawData[[self$scatterX$name64]])
                }
                if (is.something(randomData)) {
                    randomData[[self$scatterX$name64]] <- private$.rescale(self$scatterX, randomData[[self$scatterX$name64]])
                }
            }





            #### compute the levels combinations
            #### first, gets all levels of factors and covs. Then create the combinations and select the rows of the
            #### emmeans estimates needed for it. It selects the rows using the levels found in datamatic
            ### for the raw data, it selects only if the moderator is a factor, whereas all data for the
            ### continuous are retained
            #### TODO: this is abstruse, try changing it

            state <- list(
                y_range = y_range,
                x_range = x_range
            )

            dims <- sapply(moderators, function(mod) private$.datamatic$variables[[tob64(mod)]]$levels_labels, simplify = FALSE)

            if (is.something(dims)) {
                grid <- expand.grid(dims, stringsAsFactors = FALSE)
                .names <- names(grid)
                .names64 <- tob64(.names)
                selectable <- intersect(.names, self$options$factors)
                selgrid <- as.data.frame(grid[, selectable])
                .sel64 <- tob64(selectable)

                for (i in 1:nrow(grid)) {
                    label <- paste(.names, grid[i, ], sep = "=", collapse = " , ")
                    aplot <- resultsgroup$get(key = i)
                    aplot$setTitle(label)
                    sel <- paste(paste0("data$", .names64, sep = ""), paste0('"', grid[i, ], '"'), sep = "==", collapse = " & ")
                    localdata <- data[eval(parse(text = sel)), ]
                    state[["plotData"]] <- localdata

                    if (self$scatterRaw) {
                        if (length(selectable) > 0) {
                            sel <- paste(paste0("rawData$", .sel64, sep = ""), paste0('"', selgrid[i, ], '"'), sep = "==", collapse = " & ")
                            raw <- rawData[eval(parse(text = sel)), ]
                        } else {
                            raw <- rawData
                        }

                        state[["rawData"]] <- raw
                    }

                    if (!is.null(randomData)) {
                        if (length(selectable) > 0) {
                            sel <- paste(paste0("randomData$", .sel64, sep = ""), paste0('"', selgrid[i, ], '"'), sep = "==", collapse = " & ")
                            rdata <- randomData[eval(parse(text = sel)), ]
                        } else {
                            rdata <- randomData
                        }

                        selectorlist <- list(rdata[[self$scatterCluster$name64]], rdata[[self$scatterX$name64]])
                        .rnames <- c("cluster", "x", "y")
                        rdata <- stats::aggregate(rdata$y, selectorlist, mean)
                        names(rdata) <- .rnames
                        attr(rdata, "xbetween") <- xbetween
                        state[["randomData"]] <- rdata
                    }
                    aplot$setState(state)
                }
            } else {
                aplot <- resultsgroup$get(key = resultsgroup$itemKeys[[1]])
                aplot$setTitle(jmvcore::stringifyTerm(c(self$scatterX$name, self$scatterZ$name)))
                state[["plotData"]] <- data

                if (self$scatterRaw) {
                    state[["rawData"]] <- rawData
                }

                if (!is.null(randomData)) {
                    rdata <- randomData
                    selectorlist <- list(rdata[[self$scatterCluster$name64]], rdata[[self$scatterX$name64]])
                    .rnames <- c("cluster", "x", "y")
                    rdata <- stats::aggregate(rdata$y, selectorlist, mean)
                    names(rdata) <- .rnames
                    attr(rdata, "xbetween") <- xbetween
                    state[["randomData"]] <- rdata
                }

                aplot$setState(state)
            }
        },
        .initJnPlot = function() {
            if (!self$option("plot_jn")) {
                return()
            }
            if (is.null(self$scatterX)) {
                return()
            }
            if (is.null(self$scatterZ)) {
                return()
            }

            if (self$option("model_type", "ordinal") || self$option("model_type", "multinomial")) {
                self$warning <- list(
                    topic = "jnplotnotes",
                    message = paste("the Johnson-Neyman plot is not available for models of type:", self$options$model_type),
                    head = "warning"
                )
                return()
            }

            jinfo("PLOTTER: init johnson-neyman plot")
            resultsgroup <- private$.results$get("jnPlots")

            if (is.something(self$scatterModerators)) {
                plots <- simple_models_labels(self$scatterModerators, private$.operator)
                for (i in 1:nrow(plots)) {
                    resultsgroup$addItem(key = i)
                    resultsgroup$get(key = i)$setTitle(paste(names(plots), plots[i, ], sep = "=", collapse = ","))
                }
            } else {
                aplot <- resultsgroup$addItem(key = 1)
            }
        },
        .prepareJnPlot = function() {
            if (!self$option("plot_jn")) {
                return()
            }

            if (self$option("model_type", "ordinal") || self$option("model_type", "multinomial")) {
                return()
            }

            jinfo("PLOTTER: prepare johnson-neyman plot")

            if (is.null(self$scatterX)) {
                return()
            }

            if (is.null(self$scatterZ)) {
                return()
            }

            ### JN has no meaning if z or x is a factor

            if (self$scatterZ$type == "factor") {
                self$warning <- list(
                    topic = "jnplotnotes",
                    message = paste("Variable", self$scatterZ$name, "is a factor, the Johnson-Neyman plot cannot be computed."),
                    head = "warning"
                )
                return()
            }

            if (self$scatterX$type == "factor" && self$scatterX$nlevels > 2) {
                self$warning <- list(
                    topic = "jnplotnotes",
                    message = paste("Variable", self$scatterX$name, "is a factor with more than 2 levels. The Johnson-Neyman can be be computed
                                           for continuous or dichotomous predictors"),
                    head = "warning"
                )
                return()
            }


            ### JN fails if there is no interaction between x and z
            .all <- c(self$scatterZ$name64, self$scatterX$name64)
            test <- unlist(sapply(.all, function(x) !(.is.scaleDependent(private$.operator$model, x))))
            .issues <- .all[test]
            if (is.something(.issues)) {
                self$warning <- list(
                    topic = "jnplotnotes",
                    message = paste("Variable", paste(fromb64(.issues), collapse = ","), " not in interaction, the Johnson-Neyman plot cannot be produced."),
                    head = "warning"
                )
                return()
            }

            resultsgroup <- private$.results$get("jnPlots")

            expb <- FALSE
            if (self$option("plot_jn_expb")) expb <- TRUE

            model <- private$.operator$model
            if (self$scatterXscale) {
                data <- insight::get_data(model, source = "frame")
                data[[self$scatterZ$name64]] <- private$.rescale(self$scatterZ, data[[self$scatterZ$name64]])
                self$warning <- list(
                    topic = "jnplotnotes",
                    message = paste("Variable", self$scatterZ$name, " is in the original scale."), head = "info"
                )
                model <- mf.update(model, data = data)
            }
            if (is.something(self$scatterModerators)) {
                models <- simple_models(model, tob64(self$scatterModerators), obj = private$.operator)
                for (i in seq_along(models)) {
                    mod <- models[[i]]
                    datalist <- .johnson_neyman(mod, pred = self$scatterX, mod = self$scatterZ$name64, alpha = .05, expb = expb)
                    aplot <- resultsgroup$get(resultsgroup$itemKeys[[i]])
                    aplot$setState(datalist)
                }
            } else {
                aplot <- resultsgroup$get(resultsgroup$itemKeys[[1]])
                datalist <- .johnson_neyman(model, pred = self$scatterX, mod = self$scatterZ$name64, alpha = .05, expb = expb)
                aplot$setState(datalist)
            }
        },
        .prepareQqplot = function() {
            if (!self$option("qq_plot")) {
                return()
            }

            residuals <- as.numeric(scale(stats::residuals(private$.operator$model)))
            image <- private$.results$assumptions$get("qqplot")
            image$setState(list(residuals = residuals))
        },
        .prepareNormplot = function() {
            if (!self$option("norm_plot")) {
                return()
            }

            jinfo("PLOTTER: prepare norm plot")

            residuals <- as.data.frame(stats::residuals(private$.operator$model))
            image <- private$.results$assumptions$get("normPlot")
            image$setState(list(residuals = residuals))
        },
        .prepareResidplot = function() {
            if (!self$option("resid_plot")) {
                return()
            }
            residuals <- stats::residuals(private$.operator$model)
            predicted <- stats::predict(private$.operator$model)
            image <- private$.results$assumptions$get("residPlot")
            image$setState(list(data = data.frame(residuals = residuals, predicted = predicted)))
        },
        .initClusterPlots = function() {
            clusters <- private$.operator$formulaobj$clusters
            if (is.null(clusters)) {
                return()
            }
            p <- length(clusters)
            ### remove built clusters combinations
            test <- grep("[\\:\\/]", clusters, invert = TRUE)
            clusters64 <- tob64(clusters[test])
            d <- length(clusters64)
            if (p != d) {
                self$warning <- list(
                    topic = "plotnotes",
                    message = "Assumptions checking plots are not produced for crossed or nested clusters. Please specify them as dataset variables to obtain this plot.",
                    head = "warning"
                )
            }


            if (self$option("cluster_boxplot")) {
                resultsgroup <- private$.results$assumptions$clusterBoxplot
                for (cluster in clusters64) {
                    title <- paste("Clustering variable:", fromb64(cluster))
                    resultsgroup$addItem(cluster)
                    resultsgroup$get(key = cluster)$setTitle(title)
                }
            }

            if (self$option("cluster_respred")) {
                resultsgroup <- private$.results$assumptions$clusterResPred
                for (cluster in clusters64) {
                    title <- paste("Clustering variable:", fromb64(cluster))
                    resultsgroup$addItem(cluster)
                    resultsgroup$get(key = cluster)$setTitle(title)
                }
            }

            if (self$option("cluster_respred_grid")) {
                resultsgroup <- private$.results$assumptions$clusterResPredGrid
                for (cluster in clusters64) {
                    title <- paste("Clustering variable:", fromb64(cluster))
                    resultsgroup$addItem(cluster)
                    resultsgroup$get(key = cluster)$setTitle(title)
                }
            }
        },
        .prepareClusterBoxplot = function() {
            if (!self$option("cluster_boxplot")) {
                return()
            }
            ### we get the clusters from the formulaobj because the model may contain less cluster variables than selected

            clusters <- private$.operator$formulaobj$clusters
            if (is.null(clusters)) {
                return()
            }

            jinfo("PLOTTER: prepare ClusterBoxplot")

            p <- length(clusters)
            ### remove built clusters combinations
            test <- grep("[\\:\\/]", clusters, invert = TRUE)
            clusters <- tob64(clusters[test])
            d <- length(clusters)
            if (p != d) {
                self$warning <- list(
                    topic = "plotnotes",
                    message = "Assumptions checking plots are not produced for crossed or nested clusters. Please specify them as dataset variables to obtain this plot.",
                    head = "warning"
                )
            }

            resultsgroup <- private$.results$assumptions$clusterBoxplot

            data <- lme4::fortify.merMod(private$.operator$model)
            if (inherits(private$.operator$model, "lme")) {
                data$.resid <- stats::resid(private$.operator$model, type = "normalized")
            }

            for (cluster in clusters) {
                resultsgroup$get(key = cluster)$setState(list(cluster = cluster, data = data))
            }
        },
        .prepareClusterResPred = function() {
            if (!self$option("cluster_respred")) {
                return()
            }

            ### we get the clusters from the formulaobj because the model may contain less cluster variables than selected

            clusters <- private$.operator$formulaobj$clusters
            if (is.null(clusters)) {
                return()
            }

            jinfo("PLOTTER: prepare ClusterResPred")

            p <- length(clusters)
            ### remove built clusters combinations
            test <- grep("[\\:\\/]", clusters, invert = TRUE)
            clusters <- tob64(clusters[test])
            d <- length(clusters)
            if (p != d) {
                self$warning <- list(
                    topic = "plotnotes",
                    message = "Assumptions checking plots are not produced for crossed or nested clusters. Please specify them as dataset variables to obtain this plot.",
                    head = "warning"
                )
            }


            resultsgroup <- private$.results$assumptions$clusterResPred
            data <- lme4::fortify.merMod(private$.operator$model)
            if (inherits(private$.operator$model, "lme")) {
                data$.resid <- stats::resid(private$.operator$model, type = "normalized")
            }

            for (cluster in clusters) {
                title <- paste("Clustering variable:", fromb64(cluster))
                id <- cluster
                resultsgroup$get(key = id)$setTitle(title)
                resultsgroup$get(key = id)$setState(list(cluster = cluster, data = data))
            }
        },
        .prepareClusterResPredGrid = function() {
            if (!self$option("cluster_respred_grid")) {
                return()
            }
            ### we get the clusters from the formulaobj because the model may contain less cluster variables than selected

            clusters <- private$.operator$formulaobj$clusters
            if (is.null(clusters)) {
                return()
            }

            jinfo("PLOTTER: prepare ClusterResPredGrid")

            p <- length(clusters)
            ### remove built clusters combinations
            test <- grep("[\\:\\/]", clusters, invert = TRUE)
            clusters <- tob64(clusters[test])
            d <- length(clusters)
            if (p != d) {
                self$warning <- list(
                    topic = "plotnotes",
                    message = "Assumptions checking plots are not produced for crossed or nested clusters. Please specify them as dataset variables to obtain this plot.",
                    head = "warning"
                )
            }

            resultsgroup <- private$.results$assumptions$clusterResPredGrid

            data <- lme4::fortify.merMod(private$.operator$model)
            if (inherits(private$.operator$model, "lme")) {
                data$.resid <- stats::resid(private$.operator$model, type = "normalized")
            }

            for (cluster in clusters) {
                title <- paste("Clustering variable:", fromb64(cluster))
                id <- cluster
                resultsgroup$get(key = id)$setTitle(title)
                resultsgroup$get(key = id)$setState(list(cluster = cluster, data = data))
            }
        },
        .initRandHist = function() {
            if (!self$option("rand_hist")) {
                return()
            }
            jinfo("PLOTTER: init RandHist")

            clusters <- private$.operator$formulaobj$clusters
            random <- private$.operator$formulaobj$random
            random <- lapply(random, function(x) unlist(lapply(x, function(z) z[-length(z)])))

            if (is.null(clusters)) {
                return()
            }

            resultsgroup <- private$.results$assumptions$randHist

            for (i in seq_along(clusters)) {
                cluster <- clusters[i]
                vars <- random[[i]]
                i <- 0
                for (v in vars) {
                    i <- i + 1
                    title <- paste("Coefficient", v, " random across", cluster)
                    id <- tob64(paste0(cluster, i))
                    resultsgroup$addItem(id)
                    resultsgroup$get(key = id)$setTitle(title)
                }
            }
        },
        .prepareRandHist = function() {
            if (!self$option("rand_hist")) {
                return()
            }

            if (is.null(private$.operator$model)) {
                return()
            }

            jinfo("PLOTTER: prepare RandHist")

            clusters <- private$.operator$formulaobj$clusters
            random <- private$.operator$formulaobj$random
            random <- lapply(random, function(x) unlist(lapply(x, function(z) z[-length(z)])))

            if (is.null(clusters)) {
                return()
            }

            res <- lme4::ranef(private$.operator$model)
            resultsgroup <- private$.results$assumptions$randHist

            for (cluster in clusters) {
                clusterres <- res[[tob64(cluster)]]
                vars <- names(clusterres)
                i <- 0
                for (v in vars) {
                    i <- i + 1
                    data <- data.frame(clusterres[, v])
                    names(data) <- "x"
                    id <- tob64(paste0(cluster, i))
                    resultsgroup$get(key = id)$setState(list(data = data))
                }
            }
        },
        .estimate = function(x, term) {
            x64 <- tob64(x)
            term64 <- tob64(term)
            conditions <- list()
            labels <- list()
            for (.term in term64) {
                var <- private$.datamatic$variables[[.term]]
                if (var$type == "numeric") {
                    conditions[[.term]] <- var$levels
                    labels[[.term]] <- var$levels_labels
                }
            }
            xobj <- private$.datamatic$variables[[x64]]

            if (xobj$type == "numeric") {
                min <- xobj$descriptive$min
                max <- xobj$descriptive$max

                if (self$option("plot_extra")) {
                    if (is.number(self$x_range$min)) {
                        min <- self$x_range$min
                    }
                    if (is.number(self$x_range$max)) {
                        max <- self$x_range$max
                    }
                }

                conditions[[x64]] <- pretty(c(min, max), n = 30)
                if (self$option("plot_xoriginal")) {
                    self$scatterXscale <- TRUE
                    self$warning <- list(topic = "plotnotes", message = "The X-axis is in the X-variable original scale", head = "info")
                }
            }
            allterm64 <- c(x64, term64)

            mode <- NULL


            if (self$option("model_type", "ordinal")) {
                if (self$option("plot_scale", "mean.class")) {
                    mode <- "mean.class"
                } else {
                    mode <- "prob"
                    self$scatterRaw <- FALSE
                }
            }

            ### now we get the estimated means #######

            em_opts <- list(
                private$.operator$model,
                specs = allterm64,
                at = conditions,
                type = self$scatterType,
                mode = mode,
                nesting = NULL,
                options = list(level = private$.operator$ciwidth),
                data = insight::get_data(private$.operator$model, source = "frame")
            )

            ### mmblogit model data are not recognized by emmeans. We need to pass them explicetely
            if (self$option("model_type", "multinomial") & self$option(".caller", "glmer")) {
                em_opts[["data"]] <- private$.operator$model$data
            }

            if (self$option("df_method")) {
                em_opts[["lmer.df"]] <- self$options$df_method
            }

            results <- try_hard(do.call(emmeans::emmeans, em_opts))
            self$warning <- list("topic" = "plotnotes", message = results$warning)
            self$error <- list("topic" = "plotnotes", message = results$error)
            referenceGrid <- results$obj
            tableData <- as.data.frame(referenceGrid)
            ### rename the columns ####
            names(tableData) <- c(allterm64, "estimate", "se", "df", "lower", "upper")

            if (self$options$plot_around == "se") {
                tableData$lower <- tableData$estimate - tableData$se
                tableData$upper <- tableData$estimate + tableData$se
            }
            if (self$options$plot_around == "none") {
                tableData$lower <- NULL
                tableData$upper <- NULL
            }
            if (xobj$type == "factor") {
                tableData[[xobj$name64]] <- factor(tableData[[xobj$name64]])
                levels(tableData[[xobj$name64]]) <- private$.datamatic$variables[[xobj$name64]]$levels_labels
            }

            for (term in term64) {
                tableData[[term]] <- factor(tableData[[term]])
                levels(tableData[[term]]) <- private$.datamatic$variables[[term]]$levels_labels
            }
            tableData
        },
        .rescale = function(varobj, values) {
            #      len <- sapply(values,function(x)   nchar(as.character(x))-nchar(as.character(trunc(x)))-1)
            #      len <- max(min(len,na.rm = T),0)
            if (varobj$covs_scale == "centered") {
                values <- values + varobj$original_descriptive$mean
            }
            if (varobj$covs_scale == "standardized") {
                values <- varobj$original_descriptive$sd * values + varobj$original_descriptive$mean
            }
            if (varobj$covs_scale == "log") {
                values <- exp(values)
            }

            values
        },
        .fix_clusters = function(data) {
            test <- grep("[\\:\\/]", private$.operator$formulaobj$clusters)
            if (length(test) > 0) {
                cluster <- private$.operator$formulaobj$clusters[[test]]
                .clustervars <- stringr::str_split(cluster, "[\\:\\/]")[[1]]
                name64 <- tob64(cluster)
                sel <- paste0("data$", name64, "=", "paste0(", paste0("data$", tob64(.clustervars), collapse = ","), ",sep='_')")
                eval(parse(text = sel))
                data[[name64]] <- factor(data[[name64]])
                self$scatterCluster <- list(name = cluster, name64 = name64, nlevels = nlevels(data[[name64]]))
            } else {
                self$scatterCluster <- private$.datamatic$variables[[tob64(private$.operator$formulaobj$clusters[[1]])]]
            }
            return(data)
        }
    ) # end of private
) # end of class


############### addition functions  ###############


#### this is taken from `interactions` package and adjusted for making it compatible with the module graphic system
#### We do not use the package function because it does not work with all models and does not fit well with the graphic rendering mechanism
#### we cite the `interactions` package anyway because this code largely depends on its.

cbands <- function(x2, y1, y3, covy1, covy3, covy1y3, tcrit) {
    upper <- c()
    slopes <- c()
    lower <- c()
    slopesf <- function(i) {
        s <- y1 + y3 * i
        return(s)
    }
    upperf <- function(i, s) {
        u <- s + tcrit * sqrt((covy1 + 2 * i * covy1y3 +
            i^2 * covy3))
        return(u)
    }
    lowerf <- function(i, s) {
        l <- s - tcrit * sqrt((covy1 + 2 * i * covy1y3 +
            i^2 * covy3))
        return(l)
    }
    slopes <- sapply(x2, slopesf, simplify = "vector", USE.NAMES = FALSE)
    upper <- mapply(upperf, x2, slopes)
    lower <- mapply(lowerf, x2, slopes)
    out <- matrix(c(x2, slopes, lower, upper), ncol = 4)
    colnames(out) <- c("z", "slopes", "Lower", "Upper")
    out <- as.data.frame(out)
    return(out)
}


.johnson_neyman <- function(model, predobj, mod, alpha = .05, expb = FALSE) {
    pred <- predobj$name64
    if (predobj$type == "factor") {
        pred <- predobj$paramsnames64
    }
    .terms <- c(pred, mod)
    params <- stats::coef(summary(model))
    ## how is the interaction named?
    ints <- c(paste(.terms, collapse = ":"), paste(rev(.terms), collapse = ":"))
    intterm <- rownames(params)[rownames(params) %in% ints]

    .data <- mf.data(model)
    obsrange <- range(.data[[mod]])
    modsd <- stats::sd(.data[[mod]])
    modrange <- c(obsrange[1] - modsd, obsrange[2] + modsd)
    y1 <- params[rownames(params) == pred, 1]
    y3 <- params[rownames(params) == intterm, 1]
    df <- stats::df.residual(model)
    if (is.null(df)) df <- Inf # normal approximation

    alpha <- alpha / 2
    tcrit <- stats::qt(alpha, df = df)
    tcrit <- abs(tcrit)
    vmat <- stats::vcov(model)
    covy3 <- vmat[intterm, intterm]
    covy1 <- vmat[pred, pred]
    covy1y3 <- vmat[intterm, pred]
    a <- tcrit^2 * covy3 - y3^2
    b <- 2 * (tcrit^2 * covy1y3 - y1 * y3)
    c <- tcrit^2 * covy1 - y1^2
    disc <- b^2 - 4 * a * c
    failed <- FALSE
    if (disc <= 0) {
        bounds <- c(-Inf, Inf)
    } else {
        .x1 <- (-b + sqrt(disc)) / (2 * a)
        .x2 <- (-b - sqrt(disc)) / (2 * a)
        result <- c(.x1, .x2)
        bounds <- sort(result, decreasing = FALSE)
    }

    names(bounds) <- c("Lower", "Higher")
    x2 <- seq(from = modrange[1], to = modrange[2], length.out = 1000)
    cbs <- cbands(x2, y1, y3, covy1, covy3, covy1y3, tcrit)
    sigs <- which((cbs$Lower < 0 & cbs$Upper < 0) | (cbs$Lower > 0 & cbs$Upper > 0))
    insigs <- setdiff(1:1000, sigs)
    cbs$Significance <- rep(NA, nrow(cbs))
    cbs$Significance <- factor(cbs$Significance, levels = c("Insignificant", "Significant"))
    index <- 1:1000 %in% insigs
    cbs$Significance[index] <- "Insignificant"
    index <- 1:1000 %in% sigs
    cbs$Significance[index] <- "Significant"
    index <- which(cbs$Significance == "Significant")[1]
    if (!is.na(index) & index != 0) {
        inside <- (cbs[index, "z"] > bounds[1] && cbs[index, "z"] < bounds[2])
        all_sig <- NULL
        if (is.na(which(cbs$Significance == "Insignificant")[1])) {
            all_sig <- TRUE
        } else {
            all_sig <- FALSE
        }
    } else {
        inside <- FALSE
        all_sig <- TRUE
    }
    cbso1 <- cbs[cbs[, "z"] < bounds[1], ]
    cbso2 <- cbs[cbs[, "z"] > bounds[2], ]
    cbsi <- cbs[(cbs[, "z"] > bounds[1] & cbs[, "z"] < bounds[2]), ]

    ### this is the y of the straight line in the middle of the plot
    intercept <- 0
    ## here we exp() everything for plotting odd-ratios
    if (expb) {
        cbsi$slopes <- exp(cbsi$slopes)
        cbsi$Lower <- exp(cbsi$Lower)
        cbsi$Upper <- exp(cbsi$Upper)

        cbso1$slopes <- exp(cbso1$slopes)
        cbso1$Lower <- exp(cbso1$Lower)
        cbso1$Upper <- exp(cbso1$Upper)

        cbso2$slopes <- exp(cbso2$slopes)
        cbso2$Lower <- exp(cbso2$Lower)
        cbso2$Upper <- exp(cbso2$Upper)

        intercept <- 1
    }

    out <- list(
        cbso1 = cbso1,
        cbso2 = cbso2,
        cbsi = cbsi,
        inside = inside,
        failed = failed,
        all_sig = all_sig,
        bounds = bounds,
        obsrange = obsrange,
        modrange = modrange,
        intercept = intercept
    )
    return(out)
}

#### we do now use this stuff below. Keep it for future reference
.jn_scatter <- function(model, pred, mod, alpha = .05) {
    .terms <- c(pred, mod)
    mat <- attr(stats::terms(model), "factors")
    ## how is the interaction named?
    intterm <- names(which.min(which(apply(mat[rownames(mat) %in% .terms, ], 2, sum) == 2)))

    .data <- mf.data(model)
    obsrange <- range(.data[[mod]])
    modsd <- stats::sd(.data[[mod]])
    modrange <- c(obsrange[1] - modsd, obsrange[2] + modsd)
    params <- stats::coef(summary(model))
    y1 <- params[rownames(params) == pred, 1]
    y3 <- params[rownames(params) == intterm, 1]
    df <- stats::df.residual(model)
    if (is.null(df)) df <- Inf # normal approximation

    alpha <- alpha / 2
    tcrit <- stats::qt(alpha, df = df)
    tcrit <- abs(tcrit)
    vmat <- stats::vcov(model)
    covy3 <- vmat[intterm, intterm]
    covy1 <- vmat[pred, pred]
    covy1y3 <- vmat[intterm, pred]
    a <- tcrit^2 * covy3 - y3^2
    b <- 2 * (tcrit^2 * covy1y3 - y1 * y3)
    c <- tcrit^2 * covy1 - y1^2
    disc <- b^2 - 4 * a * c
    failed <- FALSE
    if (disc <= 0) {
        bounds <- c(-Inf, Inf)
    } else {
        .x1 <- (-b + sqrt(disc)) / (2 * a)
        .x2 <- (-b - sqrt(disc)) / (2 * a)
        result <- c(.x1, .x2)
        bounds <- sort(result, decreasing = FALSE)
    }
}
mcfanda/gamlj documentation built on April 5, 2025, 6:59 p.m.