@jiaminghi/charts
Version:
Lightweight charting
743 lines (538 loc) • 17.9 kB
JavaScript
import { doUpdate } from '../class/updater.class'
import { xAxisConfig, yAxisConfig } from '../config'
import { filterNonNumber, deepMerge, mergeSameStackData } from '../util'
import { deepClone } from '@jiaminghi/c-render/lib/plugin/util'
const axisConfig = { xAxisConfig, yAxisConfig }
const { min, max, abs, pow } = Math
export function axis (chart, option = {}) {
let { xAxis, yAxis, series } = option
let allAxis = []
if (xAxis && yAxis && series) {
allAxis = getAllAxis(xAxis, yAxis)
allAxis = mergeDefaultAxisConfig(allAxis)
allAxis = allAxis.filter(({ show }) => show)
allAxis = mergeDefaultBoundaryGap(allAxis)
allAxis = calcAxisLabelData(allAxis, series)
allAxis = setAxisPosition(allAxis)
allAxis = calcAxisLinePosition(allAxis, chart)
allAxis = calcAxisTickPosition(allAxis, chart)
allAxis = calcAxisNamePosition(allAxis, chart)
allAxis = calcSplitLinePosition(allAxis, chart)
}
doUpdate({
chart,
series: allAxis,
key: 'axisLine',
getGraphConfig: getLineConfig
})
doUpdate({
chart,
series: allAxis,
key: 'axisTick',
getGraphConfig: getTickConfig
})
doUpdate({
chart,
series: allAxis,
key: 'axisLabel',
getGraphConfig: getLabelConfig
})
doUpdate({
chart,
series: allAxis,
key: 'axisName',
getGraphConfig: getNameConfig
})
doUpdate({
chart,
series: allAxis,
key: 'splitLine',
getGraphConfig: getSplitLineConfig
})
chart.axisData = allAxis
}
function getAllAxis (xAxis, yAxis) {
let [allXAxis, allYAxis] = [[], []]
if (xAxis instanceof Array) {
allXAxis.push(...xAxis)
} else {
allXAxis.push(xAxis)
}
if (yAxis instanceof Array) {
allYAxis.push(...yAxis)
} else {
allYAxis.push(yAxis)
}
allXAxis.splice(2)
allYAxis.splice(2)
allXAxis = allXAxis.map((axis, i) => ({ ...axis, index: i, axis: 'x' }))
allYAxis = allYAxis.map((axis, i) => ({ ...axis, index: i, axis: 'y' }))
return [...allXAxis, ...allYAxis]
}
function mergeDefaultAxisConfig (allAxis) {
let xAxis = allAxis.filter(({ axis }) => axis === 'x')
let yAxis = allAxis.filter(({ axis }) => axis === 'y')
xAxis = xAxis.map(axis => deepMerge(deepClone(xAxisConfig), axis))
yAxis = yAxis.map(axis => deepMerge(deepClone(yAxisConfig), axis))
return [...xAxis, ...yAxis]
}
function mergeDefaultBoundaryGap (allAxis) {
const valueAxis = allAxis.filter(({ data }) => data === 'value')
const labelAxis = allAxis.filter(({ data }) => data !== 'value')
valueAxis.forEach(axis => {
if (typeof axis.boundaryGap === 'boolean') return
axis.boundaryGap = false
})
labelAxis.forEach(axis => {
if (typeof axis.boundaryGap === 'boolean') return
axis.boundaryGap = true
})
return [...valueAxis, ...labelAxis]
}
function calcAxisLabelData (allAxis, series) {
let valueAxis = allAxis.filter(({ data }) => data === 'value')
let labelAxis = allAxis.filter(({ data }) => data instanceof Array)
valueAxis = calcValueAxisLabelData(valueAxis, series)
labelAxis = calcLabelAxisLabelData(labelAxis)
return [...valueAxis, ...labelAxis]
}
function calcValueAxisLabelData (valueAxis, series) {
return valueAxis.map(axis => {
const minMaxValue = getValueAxisMaxMinValue(axis, series)
const [min, max] = getTrueMinMax(axis, minMaxValue)
const interval = getValueInterval(min, max, axis)
const { axisLabel: { formatter } } = axis
let label = []
if (min < 0 && max > 0) {
label = getValueAxisLabelFromZero(min, max, interval)
} else {
label = getValueAxisLabelFromMin(min, max, interval)
}
label = label.map(l => parseFloat(l.toFixed(2)))
return {
...axis,
maxValue: label.slice(-1)[0],
minValue: label[0],
label: getAfterFormatterLabel(label, formatter)
}
})
}
function getValueAxisMaxMinValue (axis, series) {
series = series.filter(({ show, type }) => {
if (show === false) return false
if (type === 'pie') return false
return true
})
if (series.length === 0) return [0, 0]
const { index, axis: axisType } = axis
series = mergeStackData(series)
const axisName = axisType + 'Axis'
let valueSeries = series.filter(s => s[axisName] === index)
if (!valueSeries.length) valueSeries = series
return getSeriesMinMaxValue(valueSeries)
}
function getSeriesMinMaxValue (series) {
if (!series) return
const minValue = Math.min(
...series
.map(({ data }) => Math.min(...filterNonNumber(data)))
)
const maxValue = Math.max(
...series
.map(({ data }) => Math.max(...filterNonNumber(data)))
)
return [minValue, maxValue]
}
function mergeStackData (series) {
const seriesCloned = deepClone(series, true)
series.forEach((item, i) => {
const data = mergeSameStackData(item, series)
seriesCloned[i].data = data
})
return seriesCloned
}
function getTrueMinMax ({ min, max, axis }, [minValue, maxValue]) {
let [minType, maxType] = [typeof min, typeof max]
if (!testMinMaxType(min)) {
min = axisConfig[axis + 'AxisConfig'].min
minType = 'string'
}
if (!testMinMaxType(max)) {
max = axisConfig[axis + 'AxisConfig'].max
maxType = 'string'
}
if (minType === 'string') {
min = parseInt(minValue - abs(minValue * parseFloat(min) / 100))
const lever = getValueLever(min)
min = parseFloat((min / lever - 0.1).toFixed(1)) * lever
}
if (maxType === 'string') {
max = parseInt(maxValue + abs(maxValue * parseFloat(max) / 100))
const lever = getValueLever(max)
max = parseFloat((max / lever + 0.1).toFixed(1)) * lever
}
return [min, max]
}
function getValueLever (value) {
const valueString = abs(value).toString()
const valueLength = valueString.length
const firstZeroIndex = valueString.replace(/0*$/g, '').indexOf('0')
let pow10Num = valueLength - 1
if (firstZeroIndex !== -1) pow10Num -= firstZeroIndex
return pow(10, pow10Num)
}
function testMinMaxType (val) {
const valType = typeof val
const isValidString = (valType === 'string' && /^\d+%$/.test(val))
const isValidNumber = valType === 'number'
return isValidString || isValidNumber
}
function getValueAxisLabelFromZero (min, max, interval) {
let [negative, positive] = [[], []]
let [currentNegative, currentPositive] = [0, 0]
do {
negative.push(currentNegative -= interval)
} while (currentNegative > min)
do {
positive.push(currentPositive += interval)
} while (currentPositive < max)
return [...negative.reverse(), 0, ...positive]
}
function getValueAxisLabelFromMin (min, max, interval) {
let [label, currentValue] = [[min], min]
do {
label.push(currentValue += interval)
} while (currentValue < max)
return label
}
function getAfterFormatterLabel (label, formatter) {
if (!formatter) return label
if (typeof formatter === 'string') label = label.map(l => formatter.replace('{value}', l))
if (typeof formatter === 'function') label = label.map((value, index) => formatter({ value, index }))
return label
}
function calcLabelAxisLabelData (labelAxis) {
return labelAxis.map(axis => {
const { data, axisLabel: { formatter } } = axis
return { ...axis, label: getAfterFormatterLabel(data, formatter) }
})
}
function getValueInterval (min, max, axis) {
let { interval, minInterval, maxInterval, splitNumber, axis: axisType } = axis
const config = axisConfig[axisType + 'AxisConfig']
if (typeof interval !== 'number') interval = config.interval
if (typeof minInterval !== 'number') minInterval = config.minInterval
if (typeof maxInterval !== 'number') maxInterval = config.maxInterval
if (typeof splitNumber !== 'number') splitNumber = config.splitNumber
if (typeof interval === 'number') return interval
let valueInterval = parseInt((max - min) / (splitNumber - 1))
if (valueInterval.toString().length > 1) valueInterval = parseInt(valueInterval.toString().replace(/\d$/, '0'))
if (valueInterval === 0) valueInterval = 1
if (typeof minInterval === 'number' && valueInterval < minInterval) return minInterval
if (typeof maxInterval === 'number' && valueInterval > maxInterval) return maxInterval
return valueInterval
}
function setAxisPosition (allAxis) {
const xAxis = allAxis.filter(({ axis }) => axis === 'x')
const yAxis = allAxis.filter(({ axis }) => axis === 'y')
if (xAxis[0] && !xAxis[0].position) xAxis[0].position = xAxisConfig.position
if (xAxis[1] && !xAxis[1].position) {
xAxis[1].position = xAxis[0].position === 'bottom' ? 'top' : 'bottom'
}
if (yAxis[0] && !yAxis[0].position) yAxis[0].position = yAxisConfig.position
if (yAxis[1] && !yAxis[1].position) {
yAxis[1].position = yAxis[0].position === 'left' ? 'right' : 'left'
}
return [...xAxis, ...yAxis]
}
function calcAxisLinePosition (allAxis, chart) {
const { x, y, w, h } = chart.gridArea
allAxis = allAxis.map(axis => {
const { position } = axis
let linePosition = []
if (position === 'left') {
linePosition = [[x, y], [x, y + h]].reverse()
} else if (position === 'right') {
linePosition = [[x + w, y], [x + w, y + h]].reverse()
} else if (position === 'top') {
linePosition = [[x, y], [x + w, y]]
} else if (position === 'bottom') {
linePosition = [[x, y + h], [x + w, y + h]]
}
return {
...axis,
linePosition
}
})
return allAxis
}
function calcAxisTickPosition (allAxis, chart) {
return allAxis.map(axisItem => {
let { axis, linePosition, position, label, boundaryGap } = axisItem
if (typeof boundaryGap !== 'boolean') boundaryGap = axisConfig[axis + 'AxisConfig'].boundaryGap
const labelNum = label.length
const [[startX, startY], [endX, endY]] = linePosition
const gapLength = axis === 'x' ? endX - startX : endY - startY
const gap = gapLength / (boundaryGap ? labelNum : labelNum - 1)
const tickPosition = new Array(labelNum)
.fill(0)
.map((foo, i) => {
if (axis === 'x') {
return [startX + gap * (boundaryGap ? (i + 0.5) : i), startY]
}
return [startX, startY + gap * (boundaryGap ? (i + 0.5) : i)]
})
const tickLinePosition = getTickLinePosition(axis, boundaryGap, position, tickPosition, gap)
return {
...axisItem,
tickPosition,
tickLinePosition,
tickGap: gap
}
})
}
function getTickLinePosition (axisType, boundaryGap, position, tickPosition, gap) {
let index = axisType === 'x' ? 1 : 0
let plus = 5
if (axisType === 'x' && position === 'top') plus = -5
if (axisType === 'y' && position === 'left') plus = -5
let tickLinePosition = tickPosition.map(lineStart => {
const lineEnd = deepClone(lineStart)
lineEnd[index] += plus
return [deepClone(lineStart), lineEnd]
})
if (!boundaryGap) return tickLinePosition
index = axisType === 'x' ? 0 : 1
plus = gap / 2
tickLinePosition.forEach(([lineStart, lineEnd]) => {
lineStart[index] += plus
lineEnd[index] += plus
})
return tickLinePosition
}
function calcAxisNamePosition (allAxis, chart) {
return allAxis.map(axisItem => {
let { nameGap, nameLocation, position, linePosition } = axisItem
const [lineStart, lineEnd] = linePosition
let namePosition = [...lineStart]
if (nameLocation === 'end') namePosition = [...lineEnd]
if (nameLocation === 'center') {
namePosition[0] = (lineStart[0] + lineEnd[0]) / 2
namePosition[1] = (lineStart[1] + lineEnd[1]) / 2
}
let index = 0
if (position === 'top' && nameLocation === 'center') index = 1
if (position === 'bottom' && nameLocation === 'center') index = 1
if (position === 'left' && nameLocation !== 'center') index = 1
if (position === 'right' && nameLocation !== 'center') index = 1
let plus = nameGap
if (position === 'top' && nameLocation !== 'end') plus *= -1
if (position === 'left' && nameLocation !== 'start') plus *= -1
if (position === 'bottom' && nameLocation === 'start') plus *= -1
if (position === 'right' && nameLocation === 'end') plus *= -1
namePosition[index] += plus
return {
...axisItem,
namePosition
}
})
}
function calcSplitLinePosition (allAxis, chart) {
const { w, h } = chart.gridArea
return allAxis.map(axisItem => {
const { tickLinePosition, position, boundaryGap } = axisItem
let [index, plus] = [0, w]
if (position === 'top' || position === 'bottom') index = 1
if (position === 'top' || position === 'bottom') plus = h
if (position === 'right' || position === 'bottom') plus *= -1
const splitLinePosition = tickLinePosition.map(([startPoint]) => {
const endPoint = [...startPoint]
endPoint[index] += plus
return [[...startPoint], endPoint]
})
if (!boundaryGap) splitLinePosition.shift()
return {
...axisItem,
splitLinePosition
}
})
}
function getLineConfig (axisItem) {
const { animationCurve, animationFrame, rLevel } = axisItem
return [{
name: 'polyline',
index: rLevel,
visible: axisItem.axisLine.show,
animationCurve,
animationFrame,
shape: getLineShape(axisItem),
style: getLineStyle(axisItem)
}]
}
function getLineShape (axisItem) {
const { linePosition } = axisItem
return {
points: linePosition
}
}
function getLineStyle (axisItem) {
return axisItem.axisLine.style
}
function getTickConfig (axisItem) {
const { animationCurve, animationFrame, rLevel } = axisItem
const shapes = getTickShapes(axisItem)
const style = getTickStyle(axisItem)
return shapes.map(shape => ({
name: 'polyline',
index: rLevel,
visible: axisItem.axisTick.show,
animationCurve,
animationFrame,
shape,
style
}))
}
function getTickShapes (axisItem) {
const { tickLinePosition } = axisItem
return tickLinePosition.map(points => ({ points }))
}
function getTickStyle (axisItem) {
return axisItem.axisTick.style
}
function getLabelConfig (axisItem) {
const { animationCurve, animationFrame, rLevel } = axisItem
const shapes = getLabelShapes(axisItem)
const styles = getLabelStyle(axisItem, shapes)
return shapes.map((shape, i) => ({
name: 'text',
index: rLevel,
visible: axisItem.axisLabel.show,
animationCurve,
animationFrame,
shape,
style: styles[i],
setGraphCenter: () => (void 0)
}))
}
function getLabelShapes (axisItem) {
const { label, tickPosition, position } = axisItem
return tickPosition.map((point, i) => ({
position: getLabelRealPosition(point, position),
content: label[i].toString(),
}))
}
function getLabelRealPosition (points, position) {
let [index, plus] = [0, 10]
if (position === 'top' || position === 'bottom') index = 1
if (position === 'top' || position === 'left') plus = -10
points = deepClone(points)
points[index] += plus
return points
}
function getLabelStyle (axisItem, shapes) {
const { position } = axisItem
let { style } = axisItem.axisLabel
const align = getAxisLabelRealAlign(position)
style = deepMerge(align, style)
const styles = shapes.map(({ position }) => ({
...style,
graphCenter: position
}))
return styles
}
function getAxisLabelRealAlign (position) {
if (position === 'left') return {
textAlign: 'right',
textBaseline: 'middle'
}
if (position === 'right') return {
textAlign: 'left',
textBaseline: 'middle'
}
if (position === 'top') return {
textAlign: 'center',
textBaseline: 'bottom'
}
if (position === 'bottom') return {
textAlign: 'center',
textBaseline: 'top'
}
}
function getNameConfig (axisItem) {
const { animationCurve, animationFrame, rLevel } = axisItem
return [{
name: 'text',
index: rLevel,
animationCurve,
animationFrame,
shape: getNameShape(axisItem),
style: getNameStyle(axisItem)
}]
}
function getNameShape (axisItem) {
const { name, namePosition } = axisItem
return {
content: name,
position: namePosition
}
}
function getNameStyle (axisItem) {
const { nameLocation, position, nameTextStyle: style } = axisItem
const align = getNameRealAlign(position, nameLocation)
return deepMerge(align, style)
}
function getNameRealAlign (position, location) {
if (
(position === 'top' && location === 'start') ||
(position === 'bottom' && location === 'start') ||
(position === 'left' && location === 'center')
) return {
textAlign: 'right',
textBaseline: 'middle'
}
if (
(position === 'top' && location === 'end') ||
(position === 'bottom' && location === 'end') ||
(position === 'right' && location === 'center')
) return {
textAlign: 'left',
textBaseline: 'middle'
}
if (
(position === 'top' && location === 'center') ||
(position === 'left' && location === 'end') ||
(position === 'right' && location === 'end')
) return {
textAlign: 'center',
textBaseline: 'bottom'
}
if (
(position === 'bottom' && location === 'center') ||
(position === 'left' && location === 'start') ||
(position === 'right' && location === 'start')
) return {
textAlign: 'center',
textBaseline: 'top'
}
}
function getSplitLineConfig (axisItem) {
const { animationCurve, animationFrame, rLevel } = axisItem
const shapes = getSplitLineShapes(axisItem)
const style = getSplitLineStyle(axisItem)
return shapes.map(shape => ({
name: 'polyline',
index: rLevel,
visible: axisItem.splitLine.show,
animationCurve,
animationFrame,
shape,
style
}))
}
function getSplitLineShapes (axisItem) {
const { splitLinePosition } = axisItem
return splitLinePosition.map(points => ({ points }))
}
function getSplitLineStyle (axisItem) {
return axisItem.splitLine.style
}