R/global.R

Defines functions print.CELL_META names.CELL_META add.sector.meta.data add.track.meta.data get.cell.meta.data show.index circos.info has.cell set.cell.data get.cell.data set.current.cell set.current.sector.index get.current.sector.index set.current.track.index get.current.track.index get.sector.data get.all.track.index get.all.sector.index empty_env circos.clear circos.initialize is.circos.initialized circos.par resetGlobalVariable

Documented in circos.clear circos.info circos.initialize circos.par get.all.sector.index get.all.track.index get.cell.meta.data get.current.sector.index get.current.track.index names.CELL_META print.CELL_META set.current.cell show.index

# this file contains variables and functions related to
# global variables.

.CIRCOS.ENV = new.env()

resetGlobalVariable = function() {
	assign(".SECTOR.DATA", NULL, envir = .CIRCOS.ENV)
	assign(".CELL.DATA", NULL, envir = .CIRCOS.ENV)
	assign(".CURRENT.TRACK.INDEX", 0, envir = .CIRCOS.ENV)
	assign(".CURRENT.SECTOR.INDEX", NULL, envir = .CIRCOS.ENV)
}

resetGlobalVariable()

# == title
# Parameters for the circular layout
#
# == param
# -... Arguments for the parameters, see "details" section
# -RESET reset to default values
# -READ.ONLY please ignore
# -LOCAL please ignore
# -ADD please ignore
#
# == details
# Global parameters for the circular layout. Currently supported parameters are:
#
# -``start.degree``            The starting degree from which the circle begins to draw. Note this degree is measured
#     in the standard polar coordinate which means it is always reverse-clockwise.
# -``gap.degree``             Gap between two neighbour sectors. It can be a single value or a vector. If it is a vector,
#                          the first value corresponds to the gap after the first sector.
# -``gap.after`` identical to ``gap.degree`` option, but a more understandable name. Modifying this option will also affect ``gap.degree``.
# -``track.margin``            Like ``margin`` in Cascading Style Sheets (CSS), it is the blank area
#     out of the plotting region, also outside of the borders. Since left and right margin are controlled
#     by ``gap.degree``, only bottom and top margin need to be set. And all cells in a same track share the same margins, and
#     that's why this parameter is called ``track.margin``. The value for the ``track.margin``
#     is the percentage according to the radius of the unit circle. `convert_height` can be used to set to an absolute unit (e.g cm/inche).
# -``unit.circle.segments``    Since curves are simulated by a series of straight lines,
#     this parameter controls the amount of segments to represent a curve. The minimal length
#     of the line segmentation is the length of the unit circle (``2pi``) divided by ``unit.circoe.segments``.
#     More segments means better approximation for the curves while larger size if you generate figures as PDF format.
# -``cell.padding``            Padding of the cell. Like ``padding`` in Cascading Style Sheets
#    (CSS), it is the blank area around the plotting regions, but within the borders.
#     The parameter has four values, which controls the bottom, left, top and right paddings
#     respectively. The first and the third padding
#     values are the percentages according to the radius of the unit circle and the second and
#     fourth values are degrees. Similar as ``track.margin`` option, the first and the third value
#     can be set by `convert_height` to an absolute unit.
# -``track.height``    The default height of tracks. It is the percentage according to the radius
#     of the unit circle. The height includes the top and bottom cell paddings but not the margins.
#     `convert_height` can be used to set the height to an absolute unit.
# -``points.overflow.warning`` Since each cell is in fact not a real plotting region but only
#     an ordinary rectangle, it does not eliminate points that are plotted out of
#     the region. So if some points are out of the plotting region, ``circlize`` would continue drawing the points and printing warnings. In some
#     cases, draw something out of the plotting region is useful, such as draw
#     some legend or text. Set this value to ``FALSE`` to turn off the warnings.
# -``circle.margin``  Margin in the horizontal and vertical direction. The value should be a positive numeric vector
#    and the length of it should be either 1, 2, or 4. When it has length of 1, it controls the margin on the four sides of the circle.
#    When it has length of 2, the first value controls the margin on the left and right, and the second value controls
#    the margin on the bottom and top side. When it has length of 4, the four values controls the margins on the left, right, bottom and top sides
#    of the circle. So A value of ``c(x1, x2, y1, y2)`` means ``circos.par(canvas.xlim = c(-(1+x1), 1+x2), canvas.ylim = c(-(1+y1), 1+y2))``.
# -``canvas.xlim``              The coordinate for the canvas. Because ``circlize`` draws everything (or almost everything) inside the unit circle,
#     the default ``canvas.xlim`` and ``canvas.ylim`` for the canvas would be all ``c(-1, 1)``. However, you can set it to a more broad
#     interval if you want to draw other things out of the circle. By choosing proper
#     ``canvas.xlim`` and ``canvas.ylim``, you can draw part of the circle. E.g. setting
#     ``canvas.xlim`` to ``c(0, 1)`` and ``canvas.ylim`` to ``c(0, 1)`` would only draw
#     circle in the region of (0, pi/2).
# -``canvas.ylim``              The coordinate for the canvas. By default it is ``c(-1, 1)``
# -``clock.wise``               The direction for adding sectors. Default is ``TRUE``.
# -``xaxis.clock.wise``    The direction in the x-axes for all sectors. Default is ``TRUE``.
#
# Similar as `graphics::par`, you can get the parameter values by specifying the
# names of parameters and you can set the parameter values by specifying a
# named list which contains the new values.
#
# ``gap.degree``, ``start.degree``, ``canvas.xlim``, ``canvas.ylim`` and ``clock.wise``
# only be set before the initialization of the circular layout
# (i.e. before calling `circos.initialize`) because these values will not be changed after
# adding sectors on the circle. The left and right padding for ``cell.padding`` will also be
# ignored after the initialization because all cells in a sector would share the same
# left and right paddings.
#
# == seealso
# https://jokergoo.github.io/circlize_book/book/circular-layout.html#graphic-parameters
#
# == example
# circos.par
circos.par = function(..., RESET = FALSE, READ.ONLY = NULL, LOCAL = FALSE, ADD = FALSE) {}
circos.par = setGlobalOptions(
	start.degree = list(
		.value = 0,
		.length = 1,
		.class = "numeric",
		.filter = function(x) {
			if(is.circos.initialized()){
				warning_wrap("'start.degree' can only be modified before `circos.initialize`, or maybe you forgot to call `circos.clear` in your last plot.")
			}
			return(x)
		}),
	gap.degree = list(
		.value = 1,
		.class = "numeric",
		.validate = function(x) {
			all(x >= 0 & x < 360)
		},
		.filter = function(x) {
			if(is.circos.initialized()){
				warning_wrap("'gap.degree' can only be modified before `circos.initialize`, or maybe you forgot to call `circos.clear` in your last plot.")
			}
			return(x)
		}
		),
	gap.after = list(.synonymous = "gap.degree"),
	track.margin = list(
		.value = c(0.01, 0.01),  # top margin and bottom margin, percentage
		.length = 2,
		.class = "numeric"
		),
	unit.circle.segments = 500,   #to simulate smooth curve
	cell.padding = list(
		.value = c(0.02, 1, 0.02, 1),  # percentage
		.length = c(2, 4),
		.class = "numeric",
		.filter = function(x) {
			if(length(x) == 2) x = c(x, x)
			o.cell.padding = circos.par("cell.padding")
			if(is.circos.initialized()){
				return(c(x[1], o.cell.padding[2], x[3], o.cell.padding[4]))
			} else {
				return(x)
			}
		}),
	default.track.height = list(
		.value = 0.2,
		.visible = FALSE,
		.filter = function(x) {
			warning_wrap("`default.track.height` is replaced by `track.height`, ignore this setting.")
			return(x)
		}),
	track.height = 0.2,
	points.overflow.warning = TRUE,
	circle.margin = list(
		.value = c(0, 0, 0, 0),
		.filter = function(x) {
			if(is.circos.initialized()){
				warning_wrap("'circle.margin' can only be modified before `circos.initialize`, or maybe you forgot to call `circos.clear` in your last plot.")
			}
			if(any(x <= 0)) {
				stop_wrap("The value of `circle.margin` should be positive.")
			}
			if(length(x) == 1) {
				x = rep(x, 4)
			} else if(length(x) == 2) {
				x = rep(x, each = 2)
			} else if(length(x) == 4) {

			} else {
				stop_wrap("Length of `circle.margin` can only be 1, 2, or 4.")
			}
			return(x)
		}
	),
	canvas.xlim = list(
		.value = function() {
			c(-(1 + .v$circle.margin[1]), (1 + .v$circle.margin[2]))
		},
		.filter = function(x) {
			if(is.circos.initialized()){
				warning_wrap("'canvas.xlim' can only be modified before `circos.initialize`, or maybe you forgot to call `circos.clear` in your last plot.")
			}
			return(x)
		}),
	canvas.ylim = list(
		.value = function() {
			c(-(1 + .v$circle.margin[3]), (1 + .v$circle.margin[4]))
		},
		.filter = function(x) {
			if(is.circos.initialized()){
				warning_wrap("'canvas.ylim' can only be modified before `circos.initialize`, or maybe you forgot to call `circos.clear` in your last plot.")
			}
			return(x)
		}),
	major.by.degree = 10,
	clock.wise = list(
		.value = TRUE,
		.filter = function(x) {
			if(is.circos.initialized()){
				warning_wrap("'clock.wise' can only be modified before `circos.initialize`, or maybe you forgot to call `circos.clear` in your last plot.")
			}
			return(x)
		}),
	xaxis.clock.wise = list(
		.value = TRUE,
		.filter = function(x) {
			if(is.circos.initialized() && environmentName(topenv()) != "circlize"){
				stop_wrap("'xaxis.clock.wise' can only be modified before `circos.initialize`, or maybe you forgot to call `circos.clear` in your last plot.")
			}
			return(x)
		}),
	lend = list(
		.value = NULL,
		.visible = FALSE,
		.private = TRUE),
	ljoin = list(
		.value = NULL,
		.visible = FALSE,
		.private = TRUE),
	'__tempdir__' = list(
		.value = ".",
		.private = TRUE,
		.filter = function(x) {dir.create(x, showWarnings = FALSE); return(x)},
		.visible = FALSE),
	'__omar__' = list(   # is par("mar") the default value?
		.value = FALSE,
		.private = TRUE,
		.visible = FALSE),
	'__tempenv__' = list(
		.value = new.env(parent = emptyenv()),
		.private = TRUE,
		.visible = FALSE),
	message = TRUE,
	help = list(.synonymous = "message"),
	ring = list(
		.value = FALSE,
		.private = TRUE,
		.visible = FALSE
	)
)

# before initialization, .SECTOR.DATA is NULL
is.circos.initialized = function() {
	.SECTOR.DATA = get(".SECTOR.DATA", envir = .CIRCOS.ENV)
	return(! is.null(.SECTOR.DATA))
}

# == title
# Initialize the circular layout
#
# == param
# -sectors A `factor` variable or a character vector which represent data categories
# -factors The same as ``sectors``. It will be removed in future versions. 
# -x       Data on x-axes, a vector
# -xlim    Ranges for values on x-axes, see "details" section for explanation of the format
# -sector.width Width for each sector. The length of the vector should be either 1 which means
#          all sectors have same width or as same as the number of sectors. Values for
#          the vector are relative, and they will be scaled by dividing their summation.
#          By default, it is ``NULL`` which means the width of sectors correspond to the data
#          range in sectors.
# -ring Whether the sector represented as a ring. If yes, there should only be one sector in the circle.
#
# == details
# The function allocates the sectors according to the values on x-axis.
# The number of sectors are determined by the ``factors`` and the order
# of sectors are determined by the levels of factors. In this function,
# the start and end position for each sector on the circle (measured by degree)
# are calculated according to the values on x-axis or by ``xlim``.
#
# If ``x`` is set, the length of ``x`` must be equal to the length of ``factors``.
# Then the data range for each sector are calculated from ``x`` by splitting ``factors``.
#
# If ``xlim`` is set, it should be a vector containing two numbers or a matrix with 2 columns.
# If ``xlim`` is a 2-element vector, it means all sector share the same ``xlim``.
# If ``xlim`` is a 2-column matrix, the number of rows should be equal to the number of categories
# identified by ``factors``, then each row of ``xlim`` corresponds to the data range for each sector
# and the order of rows is corresponding to the order of levels of ``factors``. If ``xlim`` is a matrix
# for which row names cover all sector names, ``xlim`` is automatically adjusted.
#
# Normally, width of sectors will be calculated internally according to the data range in sectors. But you can
# still set the width manually. However, it is not always a good idea to change the default sector width since
# the width can reflect the range of data in sectors. However, in some cases, it is useful to manually set
# the width such as you want to zoom some part of the sectors.
#
# The function finally calls `graphics::plot` with enforing aspect ratio to be 1 and be ready for adding graphics.
#
# == seealso
# https://jokergoo.github.io/circlize_book/book/circular-layout.html
circos.initialize = function(
	sectors = NULL, 
	x = NULL,
	xlim = NULL,
	sector.width = NULL,
	factors = sectors,
	ring = FALSE) {

    resetGlobalVariable()

	.SECTOR.DATA = get(".SECTOR.DATA", envir = .CIRCOS.ENV)
	.CELL.DATA = get(".CELL.DATA", envir = .CIRCOS.ENV)

	if(is.null(factors)) {
		if(is.matrix(xlim) || is.data.frame(xlim)) {
			if(is.null(rownames(xlim))) {
				stop_wrap("Since `sectors` is not specified, row names of `xlim` are taken as `sectors`, thus `xlim` should be a two-column matrix with row names.")
			} else {
				factors = rownames(xlim)
			}
		} else {
			stop_wrap("Since `sectors` is not specified, row names of `xlim` are taken as `sectors`, thus `xlim` should be a two-column matrix with row names.")
		}
	}

	if(is.numeric(factor)) {
		warning_wrap("Your `sectors` is numeric, it will be converted to characters internally.")
	}

	if(any(factors == "")) {
		stop_wrap("`sectors` cannot contain empty strings.")
	}

    if(! is.factor(factors)) {
        if(length(factors) == length(unique(factors))) {
           factors = factor(factors, levels = factors)
        } else {
            factors = factor(factors)
        }
    }
    factors = factor(as.character(factors), intersect(levels(factors), as.character(factors)))
    le = levels(factors)

    if(ring) {
    	if(length(le) != 1) {
    		stop_wrap("There should be only one sector under 'ring' mode.")
    	}
    	circos.par$ring = TRUE
    	circos.par$gap.degree = 0
    	circos.par$cell.padding = c(circos.par$cell.padding[1], 0, circos.par$cell.padding[3], 0)
    	circos.par$points.overflow.warning = FALSE
    } else {
    	circos.par$ring = FALSE
    }

    if(!is.null(x)) {
    	x = as.numeric(x)
    }

    # initialize .SECTOR.DATA
    # you can think it as the global x axis configuration
    # calculate min and max value for each sectors
    # there are several ways
	# xlim is prior than x
    if(is.vector(xlim)) {
        if(length(xlim) != 2) {
            stop_wrap("Since `xlim` is vector, it should have length of 2.")
        }

        xlim = as.numeric(xlim)

        min.value = rep(xlim[1], length(le))
        max.value = rep(xlim[2], length(le))
    } else if(is.matrix(xlim) || is.data.frame(xlim)) {
        if(dim(xlim)[1] != length(le) || dim(xlim)[2] != 2) {
            stop_wrap("Since `xlim` is a matrix, it should have same number of rows as the length of the level of `sectors` and number of columns of 2.")
        }

        if(!is.null(rownames(xlim))) {
        	if(length(setdiff(le, rownames(xlim))) == 0) {
        		xlim = xlim[le, ,drop = FALSE]
        	}
        }
        if(is.data.frame(xlim)) xlim = as.matrix(xlim)
        xlim2 = as.numeric(xlim)
    	dim(xlim2) = dim(xlim)
    	dimnames(xlim2) = dimnames(xlim)
    	xlim = xlim2

        min.value = apply(xlim, 1, function(x) x[1])
        max.value = apply(xlim, 1, function(x) x[2])
    } else if(is.vector(x)) {

        if(length(x) != length(factors)) {
            stop_wrap("Length of `x` and length of `sectors` differ.")
        }
        min.value = tapply(x, factors, min)
        max.value = tapply(x, factors, max)
    } else {
		stop_wrap("You should specify either `x` or `xlim`.")
	}

    cell.padding = circos.par("cell.padding")

	# range for sectors
    sector.range = max.value - min.value
    n.sector = length(le)

    sector = vector("list", 7)
	# for each sector, `start.degree always referto `min.value` and `end.degree` always
	# refer to `max.value` in a reverse clockwise fasion. So here `start.degree` and
	# `end.degree` also correspond to the direction.
	# So in the polar coordinate, `start.degree` would be larger than `end.degree`
    names(sector) = c("factor", "min.value", "max.value", "start.degree", "end.degree", "min.data", "max.data")
    sector[["factor"]] = le
	sector[["min.data"]] = min.value
	sector[["max.data"]] = max.value

    gap.degree = circos.par("gap.degree")
    if(!is.null(names(gap.degree))) {
    	if(length(setdiff(le, names(gap.degree))) == 0) {
    		gap.degree = gap.degree[le]
    	}
    }

	if(length(gap.degree) == 1) {
		gap.degree = rep(gap.degree, n.sector)
	} else if(length(gap.degree) != n.sector) {
		stop_wrap("Since `gap.degree` parameter has length larger than 1, it should have same length as the number of sectors.")
	}

	start.degree = circos.par("start.degree")
	clock.wise = circos.par("clock.wise")

    if(360 - sum(gap.degree) <= 0) {
		stop_wrap("Maybe your `gap.degree` is too large so that there is no space to allocate sectors.")
	}

    if(is.null(sector.width)) {
		# degree per data
		unit = (360 - sum(gap.degree)) / sum(sector.range)

		for(i in seq_len(n.sector)) {

			if(sector.range[i] == 0) {
				stop_wrap("Range of the sector ('", le[i] ,"') cannot be 0. You might need to set the second and the fourth values in `circos.par$cell.padding` to 0.")
			}

			# only to ensure value are always increasing or decreasing with the absolute degree value
			if(clock.wise) {
				sector[["start.degree"]][i] = ifelse(i == 1, start.degree, sector[["end.degree"]][i-1] - gap.degree[i-1])
				sector[["end.degree"]][i] =  sector[["start.degree"]][i] - sector.range[i]*unit
			} else {
				sector[["end.degree"]][i] = ifelse(i == 1, start.degree, sector[["start.degree"]][i-1] + gap.degree[i-1])
				sector[["start.degree"]][i] = sector[["end.degree"]][i] + sector.range[i]*unit
			}
		}
	} else {
		if(length(sector.width) == 1) {
			sector.width = rep(sector.width, n.sector)
		} else if(length(sector.width) != n.sector) {
			stop_wrap("Since you manually set the width for each sector, the length of `sector.width` should be either 1 or as same as the number of sectors.")
		}

		sector.width.percentage = sector.width / sum(sector.width)
		degree.per.sector = (360 - sum(gap.degree)) * sector.width.percentage

		if(any(degree.per.sector <= 0)) {
			stop_wrap("Maybe your `gap.degree` is too large so that there is no space to allocate sectors.")
		}

		for(i in seq_len(n.sector)) {

			if(sector.range[i] == 0) {
				stop_wrap("Range of the sector (", le[i] ,") cannot be 0.")
			}


			# only to ensure value are always increasing or decreasing with the absolute degree value
			if(clock.wise) {
				sector[["start.degree"]][i] = ifelse(i == 1, start.degree, sector[["end.degree"]][i-1] - gap.degree[i-1])
				sector[["end.degree"]][i] =  sector[["start.degree"]][i] - degree.per.sector[i]
			} else {
				sector[["end.degree"]][i] = ifelse(i == 1, start.degree, sector[["start.degree"]][i-1] + gap.degree[i-1])
				sector[["start.degree"]][i] = sector[["end.degree"]][i] + degree.per.sector[i]
			}
		}
	}
	# from start.degree, degree is increasing in a reverse-clock wise fasion
	# so, if circos is created clock wise, the forward sector would have large degrees
	# if circos is created reverse clock wise, the forward sector would have small degrees
	# just for goodlooking for the degree
	if(clock.wise) {
		sector[["start.degree"]] = sector[["start.degree"]] + 360
		sector[["end.degree"]] = sector[["end.degree"]] + 360
	}

	if(any(cell.padding[2] + cell.padding[4] >= sector[["start.degree"]] - sector[["end.degree"]])) {
		stop_wrap("Summation of cell padding on x-direction are larger than the width for some sectors. You can e.g. set 'circos.par(cell.padding = c(0.02, 0, 0.02, 0))' or remove tiny sectors.")
	}

	min.value = min.value - cell.padding[2]/(sector[["start.degree"]] - sector[["end.degree"]] - cell.padding[2] - cell.padding[4])*sector.range  # real min value
    max.value = max.value + cell.padding[4]/(sector[["start.degree"]] - sector[["end.degree"]] - cell.padding[2] - cell.padding[4])*sector.range  # real max value
    sector[["min.value"]] = min.value
    sector[["max.value"]] = max.value

    sector = as.data.frame(sector, stringsAsFactors = FALSE)
    .SECTOR.DATA = sector

    # initialize .CELL.DATA which contains information of each cell
    # if content of that cell has been created, it means that the
    # plotteing region for that cell has been created.
    .CELL.DATA = vector("list", length = length(le))
    names(.CELL.DATA) = le
    for(i in seq_along(.CELL.DATA)) {
        .CELL.DATA[[ le[i] ]] = vector("list", length = 0)
    }

	assign(".SECTOR.DATA", .SECTOR.DATA, envir = .CIRCOS.ENV)
	assign(".CELL.DATA", .CELL.DATA, envir = .CIRCOS.ENV)
	assign(".CURRENT.SECTOR.INDEX", .SECTOR.DATA[1, "factor"], envir = .CIRCOS.ENV)

    circos.par("__omar__" = FALSE)
	if(identical(par("mar"), c(5.1, 4.1, 4.1, 2.1))) {
		circos.par("__omar__" = TRUE)
		par(mar = c(1, 1, 1, 1))
	}
    # draw everything in a unit circle
	plot(circos.par("canvas.xlim"), circos.par("canvas.ylim"), type = "n", ann = FALSE, axes = FALSE, asp = 1)

	# all the information of cells would be visited through `get.cell.meta.data`
	return(invisible(NULL))
}

# == title
# Reset the circular layout parameters
#
# == details
# Because there are several
# parameters for the circular plot which can only be set before `circos.initialize`. So before you draw the next
# circular plot, you need to reset all these parameters.
#
# If you meet some errors when re-drawing the circular plot, try running this function and it will solve most of the problems.
circos.clear = function() {

	resetGlobalVariable()
	if(circos.par("__omar__")) {
		circos.par("__omar__" = FALSE)
		par(mar = c(5.1, 4.1, 4.1, 2.1))
	}
	tmpdir = circos.par("__tempdir__")
	circos.par(RESET = TRUE)
	circos.par("__tempdir__" = tmpdir)

	empty_env(circos.par("__tempenv__"))
	circos.par("__tempenv__" = new.env(parent = emptyenv()))

    return(invisible(NULL))
}

empty_env = function(env) {
	obj = ls(envir = env, all.names = TRUE)
	if(length(obj)) rm(list = obj, envir = env)
}

# == title
# Get index for all sectors
#
# == details
# It simply returns a vector of all sector index.
get.all.sector.index = function() {
	.SECTOR.DATA = get(".SECTOR.DATA", envir = .CIRCOS.ENV)
	if(is.null(.SECTOR.DATA)) {
		return(character(0))
	} else {
		return(as.vector(.SECTOR.DATA$factor))
	}
}

# == title
# Get index for all tracks
#
# == details
# It simply returns a vector of all track index.
get.all.track.index = function() {
	.CELL.DATA = get(".CELL.DATA", envir = .CIRCOS.ENV)
	if(is.null(.CELL.DATA)) {
		return(integer(0))
	} else if(all(sapply(.CELL.DATA, length) == 0)) {
		return(integer(0))
	} else {
		return(seq_len(max(sapply(.CELL.DATA[ which(sapply(.CELL.DATA, length) > 0) ], function(x) length(x)))))
	}
}

get.sector.data = function(sector.index = get.current.sector.index()) {
	sector.index = as.character(sector.index)
	.SECTOR.DATA = get(".SECTOR.DATA", envir = .CIRCOS.ENV)
    sector.data = as.vector(as.matrix(.SECTOR.DATA[.SECTOR.DATA[[1]] == sector.index, -1]))
    names(sector.data) = colnames(.SECTOR.DATA)[-1]
    return(sector.data)
}

# == title
# Get current track index
#
# == value
# Simply returns the numeric index for the current track.
get.current.track.index = function() {
	.CURRENT.TRACK.INDEX = get(".CURRENT.TRACK.INDEX", envir = .CIRCOS.ENV)
    return(.CURRENT.TRACK.INDEX)
}

set.current.track.index = function(x) {
	.CURRENT.TRACK.INDEX = x
	assign(".CURRENT.TRACK.INDEX", .CURRENT.TRACK.INDEX, envir = .CIRCOS.ENV)
    return(invisible(NULL))
}

# == title
# Get current sector index
#
# == value
# Simply returns the name of current sector
get.current.sector.index = function() {
	.CURRENT.SECTOR.INDEX = get(".CURRENT.SECTOR.INDEX", envir = .CIRCOS.ENV)
    return(.CURRENT.SECTOR.INDEX)
}

set.current.sector.index = function(x) {
	.CURRENT.SECTOR.INDEX = get(".CURRENT.SECTOR.INDEX", envir = .CIRCOS.ENV)
	if(!x %in% get.all.sector.index()) {
		stop_wrap(paste0("Cannot find ", x, " in all available sector names.\n"))
	}
    .CURRENT.SECTOR.INDEX = x
	assign(".CURRENT.SECTOR.INDEX", .CURRENT.SECTOR.INDEX, envir = .CIRCOS.ENV)
    return(invisible(NULL))
}

# == title
# Set flag to current cell
#
# == param
# -sector.index sector index
# -track.index track index
#
# == details
# After setting the current cell, all functions which need ``sector.index`` and ``track.index``
# arguments and are applied to the current cell do not need to specify the two arguments explicitly.
#
# == example
# pdf(NULL)
# circos.initialize(letters[1:8], xlim = c(0, 1))
# circos.track(ylim = c(0, 1))
# circos.info()
# set.current.cell("b", 1)
# circos.info()
# circos.clear()
# dev.off()
set.current.cell = function(sector.index, track.index) {
	sector.index = as.character(sector.index)
	set.current.sector.index(sector.index)
	set.current.track.index(track.index)
}

get.cell.data = function(sector.index = get.current.sector.index(), track.index = get.current.track.index()) {
	sector.index = as.character(sector.index)
	.CELL.DATA = get(".CELL.DATA", envir = .CIRCOS.ENV)
    .CELL.DATA[[sector.index]][[track.index]]
}

set.cell.data = function(sector.index = get.current.sector.index(), track.index = get.current.track.index(), ...) {
	sector.index = as.character(sector.index)
	
	.CELL.DATA = get(".CELL.DATA", envir = .CIRCOS.ENV)
    .CELL.DATA[[sector.index]][[track.index]] = list(...)
	assign(".CELL.DATA", .CELL.DATA, envir = .CIRCOS.ENV)
    return(invisible(NULL))
}

# whether cell in sector.index, track.index exists?
has.cell = function(sector.index, track.index) {
	sector.index = as.character(sector.index)
	
	.CELL.DATA = get(".CELL.DATA", envir = .CIRCOS.ENV)
    if(sector.index %in% names(.CELL.DATA) &&
       track.index <= length(.CELL.DATA[[sector.index]]) &&
       !is.null(.CELL.DATA[[sector.index]][[track.index]])) {
        return(TRUE)
    } else {
        return(FALSE)
    }
}

# == title
# Get information of the circular plot
#
# == param
# -sector.index Which sectors you want to look at? It can be a vector.
# -track.index  Which tracks you want to look at? It can be a vector.
# -plot         Whether to add information on the plot.
#
# == details
# It tells you the basic parameters for sectors/tracks/cells. If both ``sector.index``
# and ``track.index`` are set to ``NULL``, the function would print index for
# all sectors and all tracks. If ``sector.index`` and/or ``track.index`` are set,
# the function would print ``xlim``, ``ylim``, ``cell.xlim``, ``cell.ylim``,
# ``xplot``, ``yplot``, ``cell.width``, ``cell.height``, ``track.margin`` and ``cell.padding`` for every cell in specified sectors and tracks.
# Also, the function will print index of your current sector and current track.
#
# If ``plot`` is set to ``TRUE``, the function will plot the index of the sector and the track
# for each cell on the figure.
#
# == seealso
# https://jokergoo.github.io/circlize_book/book/circular-layout.html#circos-info-and-circos-clear
circos.info = function(sector.index = NULL, track.index = NULL, plot = FALSE) {
	sectors = get.all.sector.index()
	tracks = get.all.track.index()

	if(plot) {
		for(i in seq_along(sectors)) {
			for(j in seq_along(tracks)) {
				cell.xlim = get.cell.meta.data("cell.xlim", sector.index = sectors[i], track.index = j)
				cell.ylim = get.cell.meta.data("cell.ylim", sector.index = sectors[i], track.index = j)
				circos.text(mean(cell.xlim), mean(cell.ylim), labels = paste(sectors[i], j, sep = ":"),
					sector.index = sectors[i], track.index = j, facing = "downward")
			}
		}
	} else {
		# just print the name and xlim for each sector
		if(is.null(sector.index) && is.null(track.index)) {
			if(length(sectors)) {
				cat("All your sectors:\n")
				print(sectors)
			} else {
				cat("No sector has been created\n")
			}
			cat("\n")
			if(length(tracks)) {
				cat("All your tracks:\n")
				print(tracks)
			} else {
				cat("No track has been created\n")
			}
			cat("\n")

		} else {
			if(is.null(track.index)) {
				track.index = tracks
			} else if(is.null(sector.index)) {
				sector.index = sectors
			}
			for(i in seq_along(sector.index)) {
				for(j in seq_along(track.index)) {
					cat("sector index: '", sector.index[i], "'\n", sep = "")
					cat("track index: ", track.index[j], "\n", sep = "")
					xlim = get.cell.meta.data('xlim', sector.index[i], track.index[j])
					ylim = get.cell.meta.data('ylim', sector.index[i], track.index[j])
					cell.xlim = get.cell.meta.data("cell.xlim", sector.index[i], track.index[j])
					cell.ylim = get.cell.meta.data("cell.ylim", sector.index[i], track.index[j])
					xplot = get.cell.meta.data("xplot", sector.index[i], track.index[j])
					yplot = get.cell.meta.data("yplot", sector.index[i], track.index[j])
				    cell.width = get.cell.meta.data("cell.width", sector.index[i], track.index[j])
				    cell.height = get.cell.meta.data("cell.height", sector.index[i], track.index[j])
				    track.margin = get.cell.meta.data("track.margin", sector.index[i], track.index[j])
				    cell.padding = get.cell.meta.data("cell.padding", sector.index[i], track.index[j])
					cat("xlim: [", xlim[1], ", ", xlim[2], "]\n", sep = "")
					cat("ylim: [", ylim[1], ", ", ylim[2], "]\n", sep = "")
					cat("cell.xlim: [", cell.xlim[1], ", ", cell.xlim[2], "]\n", sep = "")
					cat("cell.ylim: [", cell.ylim[1], ", ", cell.ylim[2], "]\n", sep = "")
					cat("xplot (degree): [", xplot[1], ", ", xplot[2], "]\n", sep = "")
					cat("yplot (radius): [", yplot[1], ", ", yplot[2], "]\n", sep = "")
					cat("cell.width (degree): ", cell.width, "\n", sep = "")
					cat("cell.height (radius): ", cell.height, "\n", sep = "")
					cat("track.margin: c(", track.margin[1], ", ", track.margin[2], ")\n", sep = "")
					cat("cell.padding: c(", cell.padding[1], ", ", cell.padding[2], ", ", cell.padding[3], ", ", cell.padding[4], ")\n", sep = "")
					cat("\n")
				}
			}

		}

		if(length(get.current.sector.index())) cat("Your current sector.index is ", get.current.sector.index(), "\n", sep = "")
		if(get.current.track.index() > 0) cat("Your current track.index is ", get.current.track.index(), "\n", sep = "")
	}

}


# == title
# Label the sector index and the track index on each cell
#
# == details
# This function is deprecated, please use `circos.info` instead.
show.index = function() {
	circos.info(plot = TRUE)
	warning_wrap("`show.index` is deprecated, please use `circos.info` instead.")
}

# == title
# Get the meta data of a cell
#
# == param
# -name         Only support one name at a time, see "details" section
# -sector.index Index of the sector
# -track.index  Index of the track
#
# == details
# The following meta information for a cell can be obtained:
#
# -``sector.index``         The name (index) for the sector
# -``sector.numeric.index`` Numeric index for the sector
# -``track.index``          Numeric index for the track
# -``xlim``                 Minimal and maximal values on the x-axis
# -``ylim``                 Minimal and maximal values on the y-axis
# -``xrange``               Range of ``xlim``. It equals to ``xlim[2] - xlim[1]``
# -``yrange``               Range of ``ylim``
# -``xcenter``              Center of x-axis. It equals to ``(xlim[2] + xlim[1])/2``
# -``ycenter``              Center of y-axis
# -``cell.xlim``            Minimal and maximal values on the x-axis extended by cell paddings
# -``cell.ylim``            Minimal and maximal values on the y-axis extended by cell paddings
# -``xplot``                Degrees for right and left borders of the cell. The values ignore the direction of the circular layout (i.e. whether it is clock wise or not).
# -``yplot``                Radius for top and bottom borders of the cell.
# -``cell.width``           Width of the cell, in degrees.
# -``cell.height``          Height of the cell, simply ``yplot[2] - yplot[1]``
# -``cell.start.degree``    Same as ``xplot[1]``
# -``cell.end.degree``      Same as ``xplot[2]``
# -``cell.bottom.radius``   Same as ``yplot[1]``
# -``cell.top.radius``      Same as ``yplot[2]``
# -``track.margin``         Margin for the cell
# -``cell.padding``         Padding for the cell
#
# The function is useful when using ``panel.fun`` in `circos.track` to
# get detailed information of the current cell.
#
# == seealso
# `CELL_META` is a short version of `get.cell.meta.data`.
#
# == example
# sectors = letters[1:4]
# circos.initialize(sectors, xlim = c(0, 1))
# circos.trackPlotRegion(ylim = c(0, 1), panel.fun = function(x, y) {
#     print(get.cell.meta.data("xlim"))
# })
# print(get.cell.meta.data("xlim", sector.index = "a", track.index = 1))
# circos.clear()
get.cell.meta.data = function(name, sector.index = get.current.sector.index(),
                              track.index = get.current.track.index()) {
	
	sector.index = as.character(sector.index)

	if(length(sector.index) == 0) {
		stop_wrap("It seems the circular plot has not been initialized.")
	}
	if(length(track.index) == 0) {
		stop_wrap("It seems the track has not been created.")
	}
	if(length(sector.index) != 1) {
		stop_wrap("Length of `sector.index` should only be 1.")
	}
	if(length(track.index) != 1) {
		stop_wrap("Length of `track.index` should only be 1.")
	}
	if(track.index == 0) {
		stop_wrap("It seems the track has not been created.")
	}
	if(!any(sector.index %in% get.all.sector.index())) {
		stop_wrap("Cannot find sector: ", sector.index, ".")
	}
	if(!any(track.index %in% get.all.track.index())) {
		stop_wrap("Cannot find track: ", track.index, ".")
	}

	current.sector.data = get.sector.data(sector.index)
	current.cell.data = get.cell.data(sector.index, track.index)
	cell.padding = current.cell.data$cell.padding

	if(length(name) != 1) {
		stop_wrap("``name`` should only have length of 1.")
	}

	if(name == "xlim") {
		return(current.cell.data$xlim)
	} else if(name == "ylim") {
		return(current.cell.data$ylim)
	} else if(name == "xrange") {
		xlim = current.cell.data$xlim
		return(xlim[2] - xlim[1])
	} else if(name == "yrange") {
		ylim = current.cell.data$ylim
		return(ylim[2] - ylim[1])
	} else if(name == "xcenter") {
		xlim = current.cell.data$xlim
		return((xlim[2] + xlim[1])/2)
	} else if(name == "ycenter") {
		ylim = current.cell.data$ylim
		return((ylim[2] + ylim[1])/2)
	} else if(name == "cell.xlim") {
		return(current.cell.data$cell.xlim)
	} else if(name == "cell.ylim") {
		return(current.cell.data$cell.ylim)
	} else if(name == "sector.numeric.index") {
		return(which(get.all.sector.index() == sector.index))
	} else if(name == "sector.index") {
		return(sector.index)
	} else if(name == "track.index") {
		return(track.index)
	} else if(name == "xplot") {
		x = current.sector.data[c("start.degree", "end.degree")]
		names(x) = NULL
		return(x)
	} else if(name == "yplot") {
		return(c(current.cell.data$track.start - current.cell.data$track.height, current.cell.data$track.start))
	} else if(name == "cell.width") {
		x = current.sector.data[c("start.degree", "end.degree")]
		return((x[1] - x[2]))
	} else if(name == "cell.height") {
		y = c(current.cell.data$track.start - current.cell.data$track.height, current.cell.data$track.start)
		return(y[2] - y[1])
	} else if(name == "track.margin") {
		return(current.cell.data$track.margin)
	} else if(name == "cell.padding") {
		return(current.cell.data$cell.padding)
	} else if(name == "cell.start.degree") {
		x = current.sector.data["start.degree"]
		names(x) = NULL
		return(x)
	} else if(name == "cell.end.degree") {
		x = current.sector.data["end.degree"]
		names(x) = NULL
		return(x)
	} else if(name == "cell.bottom.radius") {
		return(current.cell.data$track.start - current.cell.data$track.height)
	} else if(name == "cell.top.radius") {
		return(current.cell.data$track.start)
	} else if(name == "bg.col") {
		return(current.cell.data$bg.col)
	} else if(name == "bg.border") {
		return(current.cell.data$bg.border)
	} else if(name == "bg.lty") {
		return(current.cell.data$bg.lty)
	} else if(name == "bg.lwd") {
		return(current.cell.data$bg.lwd)
	} else if(name == "track.height") {
		return(current.cell.data$track.height)
	} else {
		env = circos.par("__tempenv__")
		if(!is.null(env$track.meta.data)) {
			track.index = as.character(track.index)
			if(!is.null(env$track.meta.data[[track.index]])) {
				if(name %in% names(env$track.meta.data[[track.index]])) {
					return(env$track.meta.data[[track.index]][[name]])
				}
			}
		}
		if(!is.null(env$sector.meta.data)) {
			if(!is.null(env$sector.meta.data[[sector.index]])) {
				if(name %in% names(env$sector.meta.data[[sector.index]])) {
					return(env$sector.meta.data[[sector.index]][[name]])
				}
			}
		}
	}

	return(NULL)
}

add.track.meta.data = function(name, value, track.index = get.current.track.index()) {
	env = circos.par("__tempenv__")
	if(is.null(env$track.meta.data)) env$track.meta.data = list()
	track.index = as.character(track.index)
	if(is.null(env$track.meta.data[[track.index]])) env$track.meta.data[[track.index]] = list()
	env$track.meta.data[[track.index]][[name]] = value
}

add.sector.meta.data = function(name, value, sector.index = get.current.sector.index()) {
	sector.index = as.character(sector.index)
	env = circos.par("__tempenv__")
	if(is.null(env$sector.meta.data)) env$sector.meta.data = list()
	if(is.null(env$sector.meta.data[[sector.index]])) env$sector.meta.data[[sector.index]] = list()
	env$sector.meta.data[[sector.index]][[name]] = value
}

# == title (variable:CELL_META)
# Easy way to get meta data in the current cell
#
# == details
# The variable `CELL_META` can only be used to get meta data of the "current" cell.
# Basically you can simply replace  e.g. ``get.cell.meta.data("sector.index")`` to ``CELL_META$sector.index``.
#
# == seealso
# `get.cell.meta.data`
#
# == example
# pdf(NULL)
# circos.initialize("a", xlim = c(0, 1))
# circos.track(ylim = c(0, 1), panel.fun = function(x, y) {
#     print(CELL_META$sector.index)
#     print(CELL_META$xlim)
# })
# print(names(CELL_META))
# dev.off()
CELL_META = "don't use me directly"
class(CELL_META) = "CELL_META"

# == title
# Names of all meta data in the current cell
#
# == param
# -x use `CELL_META`.
#
# == example
# names(CELL_META)
names.CELL_META = function(x) {
	sector.index = get.current.sector.index()
    track.index = get.current.track.index()

	nm = c("xlim", "ylim", "xrange", "yrange", "xcenter", "ycenter", "cell.xlim", "cell.ylim",
     "sector.numeric.index", "sector.index", "track.index", "xplot", "yplot", "cell.width", "cell.height", "track.margin", "cell.padding",
     "cell.start.degree", "cell.end.degree", "cell.bottom.radius", "cell.top.radius", "bg.col", "bg.border",
     "bg.lty", "bg.lwd", "track.height")

	env = circos.par("__tempenv__")
	if(track.index > 0) {
		if(!is.null(env$track.meta.data)) {
			track.index = as.character(track.index)
			if(!is.null(env$track.meta.data[[track.index]])) {
				nm = c(nm, names(env$track.meta.data[[track.index]]))
			}
		}
	}
	if(!is.null(sector.index)) {
		if(!is.null(env$sector.meta.data)) {
			if(!is.null(env$sector.meta.data[[sector.index]])) {
				nm = c(nm, names(env$sector.meta.data[[sector.index]]))
			}
		}
	}

	return(nm)
}

# == title
# Easy to way to get meta data in the current cell
#
# == param
# -x name of the variable should be "CELL_META"
# -name name of the cell meta name
#
# == details
# The variable `CELL_META` can only be used to get meta data of the "current" cell.
# Basically you can simply replace  e.g. ``get.cell.meta.data("sector.index")`` to ``CELL_META$sector.index``.
#
# == seealso
# `get.cell.meta.data`
"$.CELL_META" = function(x, name) {
	get.cell.meta.data(name)
}

# == title
# Print CELL_META
#
# == param
# -x input
# -... additional parameters
#
print.CELL_META = function(x, ...) {
	cat(paste(strwrap("Please use in a form of `CELL_META$name` where `name` should be supported in `get.cell.meta.data()`. Type `names(CELL_META)` for supported names.\n"), collapse = "\n"), "\n")
}
jokergoo/circlize documentation built on Nov. 17, 2023, 11:32 a.m.