587 lines
17 KiB
JavaScript
587 lines
17 KiB
JavaScript
|
import Utils from '../utils/Utils'
|
||
|
|
||
|
export default class Range {
|
||
|
constructor(ctx) {
|
||
|
this.ctx = ctx
|
||
|
this.w = ctx.w
|
||
|
}
|
||
|
|
||
|
// http://stackoverflow.com/questions/326679/choosing-an-attractive-linear-scale-for-a-graphs-y-axiss
|
||
|
// This routine creates the Y axis values for a graph.
|
||
|
niceScale(yMin, yMax, ticks = 10, index = 0, NO_MIN_MAX_PROVIDED) {
|
||
|
const w = this.w
|
||
|
// Determine Range
|
||
|
let range = Math.abs(yMax - yMin)
|
||
|
|
||
|
ticks = this._adjustTicksForSmallRange(ticks, index, range)
|
||
|
|
||
|
if (ticks === 'dataPoints') {
|
||
|
ticks = w.globals.dataPoints - 1
|
||
|
}
|
||
|
|
||
|
if (
|
||
|
(yMin === Number.MIN_VALUE && yMax === 0) ||
|
||
|
(!Utils.isNumber(yMin) && !Utils.isNumber(yMax)) ||
|
||
|
(yMin === Number.MIN_VALUE && yMax === -Number.MAX_VALUE)
|
||
|
) {
|
||
|
// when all values are 0
|
||
|
yMin = 0
|
||
|
yMax = ticks
|
||
|
let linearScale = this.linearScale(yMin, yMax, ticks)
|
||
|
return linearScale
|
||
|
}
|
||
|
|
||
|
if (yMin > yMax) {
|
||
|
// if somehow due to some wrong config, user sent max less than min,
|
||
|
// adjust the min/max again
|
||
|
console.warn('axis.min cannot be greater than axis.max')
|
||
|
yMax = yMin + 0.1
|
||
|
} else if (yMin === yMax) {
|
||
|
// If yMin and yMax are identical, then
|
||
|
// adjust the yMin and yMax values to actually
|
||
|
// make a graph. Also avoids division by zero errors.
|
||
|
yMin = yMin === 0 ? 0 : yMin - 0.5 // some small value
|
||
|
yMax = yMax === 0 ? 2 : yMax + 0.5 // some small value
|
||
|
}
|
||
|
|
||
|
// Calculate Min amd Max graphical labels and graph
|
||
|
// increments. The number of ticks defaults to
|
||
|
// 10 which is the SUGGESTED value. Any tick value
|
||
|
// entered is used as a suggested value which is
|
||
|
// adjusted to be a 'pretty' value.
|
||
|
//
|
||
|
// Output will be an array of the Y axis values that
|
||
|
// encompass the Y values.
|
||
|
let result = []
|
||
|
|
||
|
if (
|
||
|
range < 1 &&
|
||
|
NO_MIN_MAX_PROVIDED &&
|
||
|
(w.config.chart.type === 'candlestick' ||
|
||
|
w.config.series[index].type === 'candlestick' ||
|
||
|
w.config.chart.type === 'boxPlot' ||
|
||
|
w.config.series[index].type === 'boxPlot' ||
|
||
|
w.globals.isRangeData)
|
||
|
) {
|
||
|
/* fix https://github.com/apexcharts/apexcharts.js/issues/430 */
|
||
|
yMax = yMax * 1.01
|
||
|
}
|
||
|
|
||
|
let tiks = ticks + 1
|
||
|
// Adjust ticks if needed
|
||
|
if (tiks < 2) {
|
||
|
tiks = 2
|
||
|
} else if (tiks > 2) {
|
||
|
tiks -= 2
|
||
|
}
|
||
|
|
||
|
// Get raw step value
|
||
|
let tempStep = range / tiks
|
||
|
// Calculate pretty step value
|
||
|
|
||
|
let mag = Math.floor(Utils.log10(tempStep))
|
||
|
let magPow = Math.pow(10, mag)
|
||
|
let magMsd = Math.round(tempStep / magPow)
|
||
|
if (magMsd < 1) {
|
||
|
magMsd = 1
|
||
|
}
|
||
|
let stepSize = magMsd * magPow
|
||
|
|
||
|
// build Y label array.
|
||
|
// Lower and upper bounds calculations
|
||
|
let lb = stepSize * Math.floor(yMin / stepSize)
|
||
|
let ub = stepSize * Math.ceil(yMax / stepSize)
|
||
|
// Build array
|
||
|
let val = lb
|
||
|
|
||
|
if (NO_MIN_MAX_PROVIDED && range > 2) {
|
||
|
while (1) {
|
||
|
result.push(val)
|
||
|
val += stepSize
|
||
|
if (val > ub) {
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
result,
|
||
|
niceMin: result[0],
|
||
|
niceMax: result[result.length - 1]
|
||
|
}
|
||
|
} else {
|
||
|
result = []
|
||
|
let v = yMin
|
||
|
result.push(v)
|
||
|
let valuesDivider = Math.abs(yMax - yMin) / ticks
|
||
|
for (let i = 0; i <= ticks; i++) {
|
||
|
v = v + valuesDivider
|
||
|
result.push(v)
|
||
|
}
|
||
|
|
||
|
if (result[result.length - 2] >= yMax) {
|
||
|
result.pop()
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
result,
|
||
|
niceMin: result[0],
|
||
|
niceMax: result[result.length - 1]
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
linearScale(yMin, yMax, ticks = 10, index) {
|
||
|
let range = Math.abs(yMax - yMin)
|
||
|
|
||
|
ticks = this._adjustTicksForSmallRange(ticks, index, range)
|
||
|
|
||
|
if (ticks === 'dataPoints') {
|
||
|
ticks = this.w.globals.dataPoints - 1
|
||
|
}
|
||
|
|
||
|
let step = range / ticks
|
||
|
if (ticks === Number.MAX_VALUE) {
|
||
|
ticks = 10
|
||
|
step = 1
|
||
|
}
|
||
|
|
||
|
let result = []
|
||
|
let v = yMin
|
||
|
|
||
|
while (ticks >= 0) {
|
||
|
result.push(v)
|
||
|
v = v + step
|
||
|
ticks -= 1
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
result,
|
||
|
niceMin: result[0],
|
||
|
niceMax: result[result.length - 1]
|
||
|
}
|
||
|
}
|
||
|
|
||
|
logarithmicScaleNice(yMin, yMax, base) {
|
||
|
// Basic validation to avoid for loop starting at -inf.
|
||
|
if (yMax <= 0) yMax = Math.max(yMin, base)
|
||
|
if (yMin <= 0) yMin = Math.min(yMax, base)
|
||
|
|
||
|
const logs = []
|
||
|
|
||
|
const logMax = Math.ceil(Math.log(yMax) / Math.log(base) + 1) // Get powers of base for our max and min
|
||
|
const logMin = Math.floor(Math.log(yMin) / Math.log(base))
|
||
|
|
||
|
for (let i = logMin; i < logMax; i++) {
|
||
|
logs.push(Math.pow(base, i))
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
result: logs,
|
||
|
niceMin: logs[0],
|
||
|
niceMax: logs[logs.length - 1]
|
||
|
}
|
||
|
}
|
||
|
|
||
|
logarithmicScale(yMin, yMax, base) {
|
||
|
// Basic validation to avoid for loop starting at -inf.
|
||
|
if (yMax <= 0) yMax = Math.max(yMin, base)
|
||
|
if (yMin <= 0) yMin = Math.min(yMax, base)
|
||
|
|
||
|
const logs = []
|
||
|
|
||
|
// Get the logarithmic range.
|
||
|
const logMax = Math.log(yMax) / Math.log(base)
|
||
|
const logMin = Math.log(yMin) / Math.log(base)
|
||
|
|
||
|
// Get the exact logarithmic range.
|
||
|
// (This is the exact number of multiples of the base there are between yMin and yMax).
|
||
|
const logRange = logMax - logMin
|
||
|
|
||
|
// Round the logarithmic range to get the number of ticks we will create.
|
||
|
// If the chosen min/max values are multiples of each other WRT the base, this will be neat.
|
||
|
// If the chosen min/max aren't, we will at least still provide USEFUL ticks.
|
||
|
const ticks = Math.round(logRange)
|
||
|
|
||
|
// Get the logarithmic spacing between ticks.
|
||
|
const logTickSpacing = logRange / ticks
|
||
|
|
||
|
// Create as many ticks as there is range in the logs.
|
||
|
for (let i = 0, logTick = logMin; i < ticks; i++, logTick += logTickSpacing) {
|
||
|
logs.push(Math.pow(base, logTick))
|
||
|
}
|
||
|
|
||
|
// Add a final tick at the yMax.
|
||
|
logs.push(Math.pow(base, logMax))
|
||
|
|
||
|
return {
|
||
|
result: logs,
|
||
|
niceMin: yMin,
|
||
|
niceMax: yMax
|
||
|
}
|
||
|
}
|
||
|
|
||
|
_adjustTicksForSmallRange(ticks, index, range) {
|
||
|
let newTicks = ticks
|
||
|
if (
|
||
|
typeof index !== 'undefined' &&
|
||
|
this.w.config.yaxis[index].labels.formatter &&
|
||
|
this.w.config.yaxis[index].tickAmount === undefined
|
||
|
) {
|
||
|
const formattedVal = this.w.config.yaxis[index].labels.formatter(1)
|
||
|
if (
|
||
|
Utils.isNumber(Number(formattedVal)) &&
|
||
|
!Utils.isFloat(formattedVal)
|
||
|
) {
|
||
|
newTicks = Math.ceil(range)
|
||
|
}
|
||
|
}
|
||
|
return newTicks < ticks ? newTicks : ticks
|
||
|
}
|
||
|
|
||
|
setYScaleForIndex(index, minY, maxY) {
|
||
|
const gl = this.w.globals
|
||
|
const cnf = this.w.config
|
||
|
|
||
|
let y = gl.isBarHorizontal ? cnf.xaxis : cnf.yaxis[index]
|
||
|
|
||
|
if (typeof gl.yAxisScale[index] === 'undefined') {
|
||
|
gl.yAxisScale[index] = []
|
||
|
}
|
||
|
|
||
|
let diff = Math.abs(maxY - minY)
|
||
|
|
||
|
if (y.logarithmic && diff <= 5) {
|
||
|
gl.invalidLogScale = true
|
||
|
}
|
||
|
|
||
|
if (y.logarithmic && diff > 5) {
|
||
|
gl.allSeriesCollapsed = false
|
||
|
gl.yAxisScale[index] = this.logarithmicScale(minY, maxY, y.logBase)
|
||
|
gl.yAxisScale[index] = y.forceNiceScale
|
||
|
? this.logarithmicScaleNice(minY, maxY, y.logBase)
|
||
|
: this.logarithmicScale(minY, maxY, y.logBase)
|
||
|
} else {
|
||
|
if (maxY === -Number.MAX_VALUE || !Utils.isNumber(maxY)) {
|
||
|
// no data in the chart. Either all series collapsed or user passed a blank array
|
||
|
gl.yAxisScale[index] = this.linearScale(0, 5, 5)
|
||
|
} else {
|
||
|
// there is some data. Turn off the allSeriesCollapsed flag
|
||
|
gl.allSeriesCollapsed = false
|
||
|
|
||
|
if ((y.min !== undefined || y.max !== undefined) && !y.forceNiceScale) {
|
||
|
// fix https://github.com/apexcharts/apexcharts.js/issues/492
|
||
|
gl.yAxisScale[index] = this.linearScale(
|
||
|
minY,
|
||
|
maxY,
|
||
|
y.tickAmount,
|
||
|
index
|
||
|
)
|
||
|
} else {
|
||
|
const noMinMaxProvided =
|
||
|
(cnf.yaxis[index].max === undefined &&
|
||
|
cnf.yaxis[index].min === undefined) ||
|
||
|
cnf.yaxis[index].forceNiceScale
|
||
|
gl.yAxisScale[index] = this.niceScale(
|
||
|
minY,
|
||
|
maxY,
|
||
|
y.tickAmount ? y.tickAmount : diff < 5 && diff > 1 ? diff + 1 : 5,
|
||
|
index,
|
||
|
// fix https://github.com/apexcharts/apexcharts.js/issues/397
|
||
|
noMinMaxProvided
|
||
|
)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
setXScale(minX, maxX) {
|
||
|
const w = this.w
|
||
|
const gl = w.globals
|
||
|
const x = w.config.xaxis
|
||
|
let diff = Math.abs(maxX - minX)
|
||
|
if (maxX === -Number.MAX_VALUE || !Utils.isNumber(maxX)) {
|
||
|
// no data in the chart. Either all series collapsed or user passed a blank array
|
||
|
gl.xAxisScale = this.linearScale(0, 5, 5)
|
||
|
} else {
|
||
|
gl.xAxisScale = this.linearScale(
|
||
|
minX,
|
||
|
maxX,
|
||
|
x.tickAmount ? x.tickAmount : diff < 5 && diff > 1 ? diff + 1 : 5,
|
||
|
0
|
||
|
)
|
||
|
}
|
||
|
return gl.xAxisScale
|
||
|
}
|
||
|
|
||
|
setMultipleYScales() {
|
||
|
const gl = this.w.globals
|
||
|
const cnf = this.w.config
|
||
|
|
||
|
const minYArr = gl.minYArr.concat([])
|
||
|
const maxYArr = gl.maxYArr.concat([])
|
||
|
|
||
|
let scalesIndices = []
|
||
|
// here, we loop through the yaxis array and find the item which has "seriesName" property
|
||
|
cnf.yaxis.forEach((yaxe, i) => {
|
||
|
let index = i
|
||
|
cnf.series.forEach((s, si) => {
|
||
|
// if seriesName matches and that series is not collapsed, we use that scale
|
||
|
// fix issue #1215
|
||
|
// proceed even if si is in gl.collapsedSeriesIndices
|
||
|
if (s.name === yaxe.seriesName) {
|
||
|
index = si
|
||
|
|
||
|
if (i !== si) {
|
||
|
scalesIndices.push({
|
||
|
index: si,
|
||
|
similarIndex: i,
|
||
|
alreadyExists: true
|
||
|
})
|
||
|
} else {
|
||
|
scalesIndices.push({
|
||
|
index: si
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
})
|
||
|
|
||
|
let minY = minYArr[index]
|
||
|
let maxY = maxYArr[index]
|
||
|
|
||
|
this.setYScaleForIndex(i, minY, maxY)
|
||
|
})
|
||
|
|
||
|
this.sameScaleInMultipleAxes(minYArr, maxYArr, scalesIndices)
|
||
|
}
|
||
|
|
||
|
sameScaleInMultipleAxes(minYArr, maxYArr, scalesIndices) {
|
||
|
const cnf = this.w.config
|
||
|
const gl = this.w.globals
|
||
|
|
||
|
// we got the scalesIndices array in the above code, but we need to filter out the items which doesn't have same scales
|
||
|
let similarIndices = []
|
||
|
scalesIndices.forEach((scale) => {
|
||
|
if (scale.alreadyExists) {
|
||
|
if (typeof similarIndices[scale.index] === 'undefined') {
|
||
|
similarIndices[scale.index] = []
|
||
|
}
|
||
|
similarIndices[scale.index].push(scale.index)
|
||
|
similarIndices[scale.index].push(scale.similarIndex)
|
||
|
}
|
||
|
})
|
||
|
|
||
|
function intersect(a, b) {
|
||
|
return a.filter((value) => b.indexOf(value) !== -1)
|
||
|
}
|
||
|
|
||
|
gl.yAxisSameScaleIndices = similarIndices
|
||
|
|
||
|
similarIndices.forEach((si, i) => {
|
||
|
similarIndices.forEach((sj, j) => {
|
||
|
if (i !== j) {
|
||
|
if (intersect(si, sj).length > 0) {
|
||
|
similarIndices[i] = similarIndices[i].concat(similarIndices[j])
|
||
|
}
|
||
|
}
|
||
|
})
|
||
|
})
|
||
|
|
||
|
// then, we remove duplicates from the similarScale array
|
||
|
let uniqueSimilarIndices = similarIndices.map((item) => {
|
||
|
return item.filter((i, pos) => item.indexOf(i) === pos)
|
||
|
})
|
||
|
|
||
|
// sort further to remove whole duplicate arrays later
|
||
|
let sortedIndices = uniqueSimilarIndices.map((s) => s.sort())
|
||
|
|
||
|
// remove undefined items
|
||
|
similarIndices = similarIndices.filter((s) => !!s)
|
||
|
|
||
|
let indices = sortedIndices.slice()
|
||
|
let stringIndices = indices.map((ind) => JSON.stringify(ind))
|
||
|
indices = indices.filter(
|
||
|
(ind, p) => stringIndices.indexOf(JSON.stringify(ind)) === p
|
||
|
)
|
||
|
|
||
|
let sameScaleMinYArr = []
|
||
|
let sameScaleMaxYArr = []
|
||
|
minYArr.forEach((minYValue, yi) => {
|
||
|
indices.forEach((scale, i) => {
|
||
|
// we compare only the yIndex which exists in the indices array
|
||
|
if (scale.indexOf(yi) > -1) {
|
||
|
if (typeof sameScaleMinYArr[i] === 'undefined') {
|
||
|
sameScaleMinYArr[i] = []
|
||
|
sameScaleMaxYArr[i] = []
|
||
|
}
|
||
|
sameScaleMinYArr[i].push({
|
||
|
key: yi,
|
||
|
value: minYValue
|
||
|
})
|
||
|
sameScaleMaxYArr[i].push({
|
||
|
key: yi,
|
||
|
value: maxYArr[yi]
|
||
|
})
|
||
|
}
|
||
|
})
|
||
|
})
|
||
|
|
||
|
let sameScaleMin = Array.apply(null, Array(indices.length)).map(
|
||
|
Number.prototype.valueOf,
|
||
|
Number.MIN_VALUE
|
||
|
)
|
||
|
let sameScaleMax = Array.apply(null, Array(indices.length)).map(
|
||
|
Number.prototype.valueOf,
|
||
|
-Number.MAX_VALUE
|
||
|
)
|
||
|
|
||
|
sameScaleMinYArr.forEach((s, i) => {
|
||
|
s.forEach((sc, j) => {
|
||
|
sameScaleMin[i] = Math.min(sc.value, sameScaleMin[i])
|
||
|
})
|
||
|
})
|
||
|
|
||
|
sameScaleMaxYArr.forEach((s, i) => {
|
||
|
s.forEach((sc, j) => {
|
||
|
sameScaleMax[i] = Math.max(sc.value, sameScaleMax[i])
|
||
|
})
|
||
|
})
|
||
|
|
||
|
minYArr.forEach((min, i) => {
|
||
|
sameScaleMaxYArr.forEach((s, si) => {
|
||
|
let minY = sameScaleMin[si]
|
||
|
let maxY = sameScaleMax[si]
|
||
|
|
||
|
if (cnf.chart.stacked) {
|
||
|
// for stacked charts, we need to add the values
|
||
|
maxY = 0
|
||
|
|
||
|
s.forEach((ind, k) => {
|
||
|
// fix incorrectly adjust y scale issue #1215
|
||
|
if (ind.value !== -Number.MAX_VALUE) {
|
||
|
maxY += ind.value
|
||
|
}
|
||
|
if (minY !== Number.MIN_VALUE) {
|
||
|
minY += sameScaleMinYArr[si][k].value
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
|
||
|
s.forEach((ind, k) => {
|
||
|
if (s[k].key === i) {
|
||
|
if (cnf.yaxis[i].min !== undefined) {
|
||
|
if (typeof cnf.yaxis[i].min === 'function') {
|
||
|
minY = cnf.yaxis[i].min(gl.minY)
|
||
|
} else {
|
||
|
minY = cnf.yaxis[i].min
|
||
|
}
|
||
|
}
|
||
|
if (cnf.yaxis[i].max !== undefined) {
|
||
|
if (typeof cnf.yaxis[i].max === 'function') {
|
||
|
maxY = cnf.yaxis[i].max(gl.maxY)
|
||
|
} else {
|
||
|
maxY = cnf.yaxis[i].max
|
||
|
}
|
||
|
}
|
||
|
|
||
|
this.setYScaleForIndex(i, minY, maxY)
|
||
|
}
|
||
|
})
|
||
|
})
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// experimental feature which scales the y-axis to a min/max based on x-axis range
|
||
|
autoScaleY(ctx, yaxis, e) {
|
||
|
if (!ctx) {
|
||
|
ctx = this
|
||
|
}
|
||
|
|
||
|
const w = ctx.w
|
||
|
|
||
|
if (w.globals.isMultipleYAxis || w.globals.collapsedSeries.length) {
|
||
|
// The autoScale option for multiple y-axis is turned off as it leads to buggy behavior.
|
||
|
// Also, when a series is collapsed, it results in incorrect behavior. Hence turned it off for that too - fixes apexcharts.js#795
|
||
|
console.warn('autoScaleYaxis is not supported in a multi-yaxis chart.')
|
||
|
return yaxis
|
||
|
}
|
||
|
|
||
|
const seriesX = w.globals.seriesX[0]
|
||
|
|
||
|
let isStacked = w.config.chart.stacked
|
||
|
|
||
|
yaxis.forEach((yaxe, yi) => {
|
||
|
let firstXIndex = 0
|
||
|
|
||
|
for (let xi = 0; xi < seriesX.length; xi++) {
|
||
|
if (seriesX[xi] >= e.xaxis.min) {
|
||
|
firstXIndex = xi
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
|
||
|
let initialMin = w.globals.minYArr[yi]
|
||
|
let initialMax = w.globals.maxYArr[yi]
|
||
|
let min, max
|
||
|
|
||
|
let stackedSer = w.globals.stackedSeriesTotals
|
||
|
|
||
|
w.globals.series.forEach((serie, sI) => {
|
||
|
let firstValue = serie[firstXIndex]
|
||
|
|
||
|
if (isStacked) {
|
||
|
firstValue = stackedSer[firstXIndex]
|
||
|
min = max = firstValue
|
||
|
|
||
|
stackedSer.forEach((y, yI) => {
|
||
|
if (seriesX[yI] <= e.xaxis.max && seriesX[yI] >= e.xaxis.min) {
|
||
|
if (y > max && y !== null) max = y
|
||
|
if (serie[yI] < min && serie[yI] !== null) min = serie[yI]
|
||
|
}
|
||
|
})
|
||
|
} else {
|
||
|
min = max = firstValue
|
||
|
|
||
|
serie.forEach((y, yI) => {
|
||
|
if (seriesX[yI] <= e.xaxis.max && seriesX[yI] >= e.xaxis.min) {
|
||
|
let valMin = y
|
||
|
let valMax = y
|
||
|
w.globals.series.forEach((wS, wSI) => {
|
||
|
if (y !== null) {
|
||
|
valMin = Math.min(wS[yI], valMin)
|
||
|
valMax = Math.max(wS[yI], valMax)
|
||
|
}
|
||
|
})
|
||
|
if (valMax > max && valMax !== null) max = valMax
|
||
|
if (valMin < min && valMin !== null) min = valMin
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
|
||
|
if (min === undefined && max === undefined) {
|
||
|
min = initialMin
|
||
|
max = initialMax
|
||
|
}
|
||
|
min *= min < 0 ? 1.1 : 0.9
|
||
|
max *= max < 0 ? 0.9 : 1.1
|
||
|
|
||
|
if (max < 0 && max < initialMax) {
|
||
|
max = initialMax
|
||
|
}
|
||
|
if (min < 0 && min > initialMin) {
|
||
|
min = initialMin
|
||
|
}
|
||
|
|
||
|
if (yaxis.length > 1) {
|
||
|
yaxis[sI].min = yaxe.min === undefined ? min : yaxe.min
|
||
|
yaxis[sI].max = yaxe.max === undefined ? max : yaxe.max
|
||
|
} else {
|
||
|
yaxis[0].min = yaxe.min === undefined ? min : yaxe.min
|
||
|
yaxis[0].max = yaxe.max === undefined ? max : yaxe.max
|
||
|
}
|
||
|
})
|
||
|
})
|
||
|
|
||
|
return yaxis
|
||
|
}
|
||
|
}
|