UNPKG

@toast-ui/chart

Version:

TOAST UI Application: Chart

350 lines (349 loc) 16.4 kB
import Component from "./component"; import { isNumber, pick, isNull, calculateSizeWithPercentString, deepCopy, } from "../helpers/utils"; import { message } from "../message"; import { makeGroupedSectorResponderModel } from "../helpers/responders"; import { getRadialAnchorPosition, makeAnchorPositionParam, withinRadian, getRadiusRanges, DEGREE_360, DEGREE_0, DEGREE_NEGATIVE_90, } from "../helpers/sector"; import { getActiveSeriesMap } from "../helpers/legend"; import { getDataLabelsOptions } from "../helpers/dataLabels"; import { getRGBA } from "../helpers/color"; import { getTotalAngle } from "../helpers/pieSeries"; import { isAvailableShowTooltipInfo } from "../helpers/validation"; export default class RadialBarSeries extends Component { constructor() { super(...arguments); this.models = {}; this.activatedResponders = []; this.eventDetectType = 'point'; this.onMouseoutComponent = () => { this.eventBus.emit('seriesPointHovered', { models: [], name: this.name }); this.eventBus.emit('renderHoveredSeries', { models: [], name: this.name }); this.eventBus.emit('needDraw'); }; this.selectSeries = (info) => { var _a; const { index, seriesIndex } = info; const isAvailable = isNumber(index) && (this.eventDetectType === 'grouped' || isNumber(seriesIndex)); if (!isAvailable) { return; } const models = this.eventDetectType === 'grouped' ? [ ...this.getGroupedSector([this.responders[index]], 'select'), ...this.getRadialBarSectorModelsFromResponders([this.responders[index]]), ] : (_a = this.getResponderModelsWithTheme([this.tooltipSectorMap[index][seriesIndex]], 'select'), (_a !== null && _a !== void 0 ? _a : [])); if (!models.length) { throw new Error(message.SELECT_SERIES_API_INDEX_ERROR); } this.eventBus.emit('renderSelectedSeries', { models: models, name: this.name, eventDetectType: this.eventDetectType, }); this.eventBus.emit('needDraw'); }; this.showTooltip = (info) => { const { index, seriesIndex } = info; if (!isAvailableShowTooltipInfo(info, this.eventDetectType, 'radialBar')) { return; } const models = this.eventDetectType === 'grouped' ? this.getGroupedSector([this.responders[index]], 'hover') : this.getResponderModelsWithTheme([this.tooltipSectorMap[index][seriesIndex]], 'hover'); if (!models.length) { return; } this.eventBus.emit('renderHoveredSeries', { models, name: this.name, eventDetectType: this.eventDetectType, }); this.activatedResponders = this.eventDetectType === 'grouped' ? this.tooltipSectorMap[index] : models; this.eventBus.emit('seriesPointHovered', { models: this.activatedResponders, name: this.name }); this.eventBus.emit('needDraw'); }; } initUpdate(delta) { if (!this.drawModels) { return; } const { angle: { start: startAngle, total: totalAngle }, } = this.circularAxis; let currentDegree; Object.keys(this.models).forEach((category) => { const index = this.models[category].findIndex(({ clockwise, degree: { start, end } }) => { currentDegree = clockwise ? startAngle + totalAngle * delta : startAngle - totalAngle * delta; return withinRadian(clockwise, start, end, currentDegree); }); this.syncEndAngle(index < 0 ? this.models[category].length : index, category); if (index !== -1) { this.drawModels[category][index].degree.end = currentDegree; } }); } syncEndAngle(index, category) { if (index < 1) { return; } for (let i = 0; i < index; i += 1) { const prevTargetEndDegree = this.models[category][i].degree.end; if (this.drawModels[category][i].degree.end !== prevTargetEndDegree) { this.drawModels[category][i].degree.end = prevTargetEndDegree; } } } initialize() { this.type = 'series'; this.name = 'radialBar'; this.eventBus.on('selectSeries', this.selectSeries); this.eventBus.on('showTooltip', this.showTooltip); this.eventBus.on('hideTooltip', this.onMouseoutComponent); } render(chartState) { var _a, _b; const { layout, series, legend, options, theme, stackSeries, scale, radialAxes } = chartState; const categories = (_a = chartState.categories, (_a !== null && _a !== void 0 ? _a : [])); if (!series.radialBar || !stackSeries.radialBar) { throw new Error(message.noDataError(this.name)); } this.theme = theme.series.radialBar; this.rect = layout.plot; this.activeSeriesMap = getActiveSeriesMap(legend); this.selectable = this.getSelectableOption(options); this.setEventDetectType(options); const initialCategoryMap = categories.reduce((acc, category) => { if (!acc[category]) { acc[category] = []; } return acc; }, {}); const seriesData = series.radialBar.data; this.circularAxis = radialAxes.circularAxis; const verticalAxisData = radialAxes.verticalAxis; const renderOptions = this.makeRenderOptions(verticalAxisData, scale.circularAxis, (_b = options) === null || _b === void 0 ? void 0 : _b.series); const { categoryMap, seriesModels } = this.makeSeriesModelData(seriesData, stackSeries.radialBar.stackData, renderOptions, initialCategoryMap); const tooltipData = this.makeTooltipData(seriesModels, categories); this.models = categoryMap; if (!this.drawModels) { this.initDrawModels(categoryMap); } if (getDataLabelsOptions(options, this.name).visible) { const dataLabelData = seriesModels.reduce((acc, data) => { return [...acc, Object.assign(Object.assign({}, data), { type: 'sector', theme: this.theme.dataLabels })]; }, []); this.renderDataLabels(dataLabelData); } this.tooltipSectorMap = this.makeTooltipSectorMap(seriesModels, tooltipData); this.responders = this.makeResponders(verticalAxisData.radius.ranges, seriesModels, renderOptions, categories, tooltipData); } initDrawModels(categoryMap) { this.drawModels = {}; Object.keys(categoryMap).forEach((category) => { this.drawModels[category] = categoryMap[category].map((m) => (Object.assign(Object.assign({}, m), { degree: Object.assign(Object.assign({}, m.degree), { end: m.degree.start }) }))); }); } makeResponders(radiusRanges, seriesModels, renderOptions, categories, tooltipData) { return this.eventDetectType === 'grouped' ? makeGroupedSectorResponderModel(radiusRanges, renderOptions, categories) : seriesModels.map((m, index) => (Object.assign(Object.assign({}, m), { data: Object.assign({}, tooltipData[index]) }))); } makeTooltipSectorMap(seriesModels, tooltipData) { return seriesModels.reduce((acc, cur, index) => { const categoryIndex = cur.index; if (!acc[categoryIndex]) { acc[categoryIndex] = []; } acc[categoryIndex].push(Object.assign(Object.assign({}, cur), { data: Object.assign({}, tooltipData[index]) })); return acc; }, {}); } setEventDetectType(options) { var _a, _b; if ((_b = (_a = options) === null || _a === void 0 ? void 0 : _a.series) === null || _b === void 0 ? void 0 : _b.eventDetectType) { this.eventDetectType = options.series.eventDetectType; } } getBarWidth(tickDistance, axisSize) { const { barWidth } = this.theme; const DEFAULT_PADDING = 5; return barWidth ? Math.min(tickDistance, calculateSizeWithPercentString(axisSize, barWidth)) : tickDistance - DEFAULT_PADDING * 2; } makeRenderOptions({ axisSize, centerX, centerY, tickDistance, radius: { ranges }, angle: { start, end }, }, scale, options) { var _a, _b; const { limit: { max }, stepSize, } = scale; const clockwise = (_b = (_a = options) === null || _a === void 0 ? void 0 : _a.clockwise, (_b !== null && _b !== void 0 ? _b : true)); const totalAngle = getTotalAngle(clockwise, start, end); const barWidth = this.getBarWidth(tickDistance, axisSize); const padding = (tickDistance - barWidth) / 2; const scaleMaxLimitValue = max + (totalAngle < DEGREE_360 ? DEGREE_0 : stepSize); return { clockwise, centerX, centerY, radiusRanges: getRadiusRanges(ranges, padding), angleRange: { start, end, }, totalAngle, scaleMaxLimitValue, startAngle: start, }; } makeSeriesModelData(seriesData, stackSeriesData, renderOptions, initialCategoryMap) { const { clockwise, centerX, centerY, radiusRanges, totalAngle, scaleMaxLimitValue, startAngle, } = renderOptions; const defaultStartDegree = startAngle; const { lineWidth, strokeStyle } = this.theme; const sectorModels = []; const categories = Object.keys(initialCategoryMap); const categoryMap = deepCopy(initialCategoryMap); stackSeriesData.forEach(({ values }, categoryIndex) => { const { inner, outer } = radiusRanges[categoryIndex]; values.forEach((value, seriesIndex) => { if (!isNull(value)) { const degree = Math.max((value / scaleMaxLimitValue) * totalAngle, 1) * (clockwise ? 1 : -1); const prevModel = sectorModels[sectorModels.length - 1]; const startDegree = seriesIndex && prevModel ? prevModel.degree.end : defaultStartDegree; const endDegree = clockwise ? Math.min(startDegree + degree, DEGREE_360) : Math.max(startDegree + degree, DEGREE_0); const { name, color: seriesColor } = seriesData[seriesIndex]; const color = this.getSeriesColor(name, seriesColor); const sectorModel = { type: 'sector', name, color: color, x: centerX, y: centerY, degree: { start: startDegree, end: endDegree, }, radius: { inner, outer, }, value, style: [{ strokeStyle }], lineWidth, clockwise, totalAngle, seriesColor, seriesIndex, index: categoryIndex, drawingStartAngle: DEGREE_NEGATIVE_90, }; categoryMap[categories[categoryIndex]].push(sectorModel); sectorModels.push(sectorModel); } }); }); return { seriesModels: sectorModels, categoryMap }; } getSeriesColor(name, color) { const { select, areaOpacity } = this.theme; const active = this.activeSeriesMap[name]; const selected = Object.values(this.activeSeriesMap).some((elem) => !elem); return selected ? getRGBA(color, active ? select.areaOpacity : select.restSeries.areaOpacity) : getRGBA(color, areaOpacity); } makeTooltipData(seriesModels, categories) { const tooltipData = []; seriesModels.forEach(({ seriesColor, name, value, index }) => { if (!isNull(value)) { tooltipData.push({ label: name, color: seriesColor, value: value, category: isNumber(index) ? categories[index] : '', }); } }); return tooltipData; } makeTooltipResponder(responders) { const categories = Object.keys(this.models); return responders.map((responder) => (Object.assign(Object.assign({}, responder), getRadialAnchorPosition(makeAnchorPositionParam('center', this.models[categories[responder.index]].find(({ name }) => name === responder.name)))))); } getRadialBarSectorModelsFromResponders(responders) { var _a; if (!responders.length) { return []; } return _a = this.tooltipSectorMap[responders[0].index], (_a !== null && _a !== void 0 ? _a : []); } getGroupedSector(responders, type) { const RadialBarSectorModels = this.getRadialBarSectorModelsFromResponders(responders); const { color, opacity } = this.theme[type].groupedSector; return RadialBarSectorModels.length ? responders.map((m) => (Object.assign(Object.assign({}, m), { color: getRGBA(color, opacity) }))) : []; } onMousemoveGroupedType(responders) { const RadialBarSectorModels = this.getRadialBarSectorModelsFromResponders(responders); this.eventBus.emit('renderHoveredSeries', { models: this.getGroupedSector(responders, 'hover'), name: this.name, eventDetectType: this.eventDetectType, }); this.activatedResponders = RadialBarSectorModels; } onMousemove({ responders }) { if (this.eventDetectType === 'grouped') { this.onMousemoveGroupedType(responders); } else { this.eventBus.emit('renderHoveredSeries', { models: this.getResponderModelsWithTheme(responders, 'hover'), name: this.name, }); this.activatedResponders = this.makeTooltipResponder(responders); } this.eventBus.emit('seriesPointHovered', { models: this.activatedResponders, name: this.name, eventDetectType: this.eventDetectType, }); this.eventBus.emit('needDraw'); } onClick({ responders }) { if (this.selectable) { let models; if (this.eventDetectType === 'grouped') { models = [ ...this.getGroupedSector(responders, 'select'), ...this.getRadialBarSectorModelsFromResponders(responders), ]; } else { models = this.getResponderModelsWithTheme(responders, 'select'); } this.eventBus.emit('renderSelectedSeries', { models, name: this.name, eventDetectType: this.eventDetectType, }); this.eventBus.emit('needDraw'); } } getResponderModelsWithTheme(responders, type) { const theme = this.theme[type]; const lineWidth = theme.lineWidth; const isSameLineWidth = this.theme.lineWidth === lineWidth; const thickness = isSameLineWidth ? 0 : lineWidth * 0.5; return responders.map((m) => { var _a, _b; return (Object.assign(Object.assign({}, m), { color: (_b = (_a = theme) === null || _a === void 0 ? void 0 : _a.color, (_b !== null && _b !== void 0 ? _b : m.color)), lineWidth: lineWidth, style: [ pick(theme, 'strokeStyle', 'shadowBlur', 'shadowColor', 'shadowOffsetX', 'shadowOffsetY'), ], radius: { inner: Math.max(m.radius.inner - thickness, 0), outer: m.radius.outer + thickness, } })); }); } }