@toast-ui/chart
Version:
TOAST UI Application: Chart
420 lines (419 loc) • 18.4 kB
JavaScript
import { isUndefined, sum, deepMergedCopy, range, isNumber } from "../helpers/utils";
import { getLegendItemHeight, LEGEND_CHECKBOX_SIZE, LEGEND_ICON_SIZE, LEGEND_ITEM_MARGIN_X, LEGEND_MARGIN_X, } from "../brushes/legend";
import { getTextWidth } from "../helpers/calculator";
import { isVerticalAlign, padding } from "./layout";
import { SPECTRUM_LEGEND_LABEL_HEIGHT, spectrumLegendBar, spectrumLegendTooltip, } from "../brushes/spectrumLegend";
import { hasNestedPieSeries } from "../helpers/pieSeries";
import { extend } from "./store";
import { getTitleFontString } from "../helpers/style";
import { makeDefaultTheme } from "../helpers/theme";
import { isNoData } from "../helpers/validation";
import { getIconType, getLegendAlign, showCheckbox, showCircleLegend, showLegend, } from "../helpers/legend";
const INITIAL_LEGEND_WIDTH = 100;
const INITIAL_CIRCLE_LEGEND_WIDTH = 150;
const COMPONENT_HEIGHT_EXCEPT_Y_AXIS = 100;
const ELLIPSIS_DOT_TEXT = '...';
const WIDEST_TEXT = 'W'; // The widest text width in Arial font.
const NUMBER_OF_BOTH_SIDES = 2;
function recalculateLegendWhenHeightOverflows(params, legendHeight) {
const { legendWidths, itemHeight } = params;
const totalHeight = legendWidths.length * itemHeight;
const columnCount = Math.ceil(totalHeight / legendHeight);
const rowCount = legendWidths.length / columnCount;
let legendWidth = 0;
range(0, columnCount).forEach((count) => {
legendWidth += Math.max(...legendWidths.slice(count * rowCount, (count + 1) * rowCount));
});
legendWidth += LEGEND_ITEM_MARGIN_X * (columnCount - 1);
return { legendWidth, legendHeight: rowCount * itemHeight + padding.Y, columnCount, rowCount };
}
function recalculateLegendWhenWidthOverflows(params, prevLegendWidth) {
const { legendWidths, itemHeight } = params;
let columnCount = 0;
let legendWidth = 0;
const { rowCount } = legendWidths.reduce((acc, width) => {
const widthWithMargin = LEGEND_ITEM_MARGIN_X + width;
if (acc.totalWidth + width > prevLegendWidth) {
acc.totalWidth = widthWithMargin;
acc.rowCount += 1;
acc.columnCount = 1;
columnCount = Math.max(columnCount, acc.columnCount);
}
else {
acc.totalWidth += widthWithMargin;
acc.columnCount += 1;
}
legendWidth = Math.max(legendWidth, acc.totalWidth);
return acc;
}, { totalWidth: 0, rowCount: 1, columnCount: 0 });
return { legendHeight: itemHeight * rowCount, rowCount, columnCount, legendWidth };
}
function calculateLegendSize(params) {
if (!params.visible) {
return { legendWidth: 0, legendHeight: 0, rowCount: 0, columnCount: 0 };
}
const { chart, verticalAlign, legendWidths } = params;
const { legendWidth, isOverflow: widthOverflow } = calculateLegendWidth(params);
const { legendHeight, isOverflow: heightOverflow } = calculateLegendHeight(params);
const columnCount = verticalAlign ? legendWidths.length : 1;
const rowCount = verticalAlign ? Math.ceil(legendWidth / chart.width) : legendWidths.length;
if (widthOverflow) {
return recalculateLegendWhenWidthOverflows(params, legendWidth / rowCount);
}
if (heightOverflow) {
return recalculateLegendWhenHeightOverflows(params, legendHeight);
}
return { legendWidth, legendHeight, columnCount, rowCount };
}
function calculateLegendHeight(params) {
const { verticalAlign, itemHeight, legendWidths } = params;
const { height: chartHeight } = getDefaultLegendSize(params);
let legendHeight;
let isOverflow = false;
if (verticalAlign) {
legendHeight = chartHeight;
}
else {
const totalHeight = legendWidths.length * itemHeight;
isOverflow = chartHeight < totalHeight;
legendHeight = isOverflow ? chartHeight : totalHeight;
}
return { legendHeight, isOverflow };
}
function getSpectrumLegendWidth(legendWidths, chartWidth, verticalAlign) {
if (verticalAlign) {
const labelAreaWidth = sum(legendWidths);
return Math.max(chartWidth / 4, labelAreaWidth);
}
const spectrumAreaWidth = (spectrumLegendTooltip.PADDING + spectrumLegendBar.PADDING + padding.X) * NUMBER_OF_BOTH_SIDES +
spectrumLegendTooltip.POINT_HEIGHT +
spectrumLegendBar.HEIGHT;
return Math.max(...legendWidths) + spectrumAreaWidth;
}
function getSpectrumLegendHeight(itemHeight, chartHeight, verticalAlign) {
return verticalAlign
? SPECTRUM_LEGEND_LABEL_HEIGHT +
spectrumLegendBar.PADDING * NUMBER_OF_BOTH_SIDES +
spectrumLegendTooltip.POINT_HEIGHT +
spectrumLegendTooltip.HEIGHT +
padding.Y
: (chartHeight * 3) / 4;
}
function getNormalLegendWidth(params) {
const { initialWidth, legendWidths, checkbox, verticalAlign } = params;
let isOverflow = false;
let legendWidth;
if (verticalAlign) {
const { width: chartWidth } = getDefaultLegendSize(params);
const totalWidth = sum(legendWidths) + LEGEND_ITEM_MARGIN_X * (legendWidths.length - 1);
isOverflow = totalWidth > chartWidth;
legendWidth = totalWidth;
}
else {
const labelAreaWidth = Math.max(...legendWidths);
legendWidth =
(checkbox ? LEGEND_CHECKBOX_SIZE + LEGEND_MARGIN_X : 0) +
LEGEND_ICON_SIZE +
LEGEND_MARGIN_X +
Math.max(labelAreaWidth, initialWidth);
}
return { legendWidth, isOverflow };
}
function calculateLegendWidth(params) {
var _a, _b;
const { options, visible } = params;
const legendOptions = (_a = options) === null || _a === void 0 ? void 0 : _a.legend;
if (!visible) {
return { legendWidth: 0, isOverflow: false };
}
if ((_b = legendOptions) === null || _b === void 0 ? void 0 : _b.width) {
return { legendWidth: legendOptions.width, isOverflow: false };
}
return getNormalLegendWidth(params);
}
function getDefaultLegendSize(params) {
const { verticalAlign, chart, itemHeight, initialWidth, circleLegendVisible } = params;
const restAreaHeight = COMPONENT_HEIGHT_EXCEPT_Y_AXIS + (circleLegendVisible ? INITIAL_CIRCLE_LEGEND_WIDTH : 0); // rest area temporary value (yAxisTitle.height + xAxis.height + circleLegend.height)
return verticalAlign
? { width: chart.width - padding.X * NUMBER_OF_BOTH_SIDES, height: itemHeight }
: {
width: initialWidth,
height: chart.height - restAreaHeight,
};
}
function getNestedPieLegendLabelsInfo(series, legendInfo) {
const result = [];
const maxTextLengthWithEllipsis = getMaxTextLengthWithEllipsis(legendInfo);
series.pie.forEach(({ data }) => {
data.forEach(({ name, parentName, visible }) => {
if (!parentName) {
const { width, viewLabel } = getViewLabelInfo(legendInfo, name, maxTextLengthWithEllipsis);
result.push({
label: name,
type: 'pie',
checked: (visible !== null && visible !== void 0 ? visible : true),
viewLabel,
width,
});
}
});
});
return result;
}
function getMaxTextLengthWithEllipsis(legendInfo) {
var _a, _b;
const { legendOptions, font, checkboxVisible } = legendInfo;
const width = (_b = (_a = legendOptions) === null || _a === void 0 ? void 0 : _a.item) === null || _b === void 0 ? void 0 : _b.width;
if (isUndefined(width)) {
return;
}
const checkboxWidth = checkboxVisible ? LEGEND_CHECKBOX_SIZE + LEGEND_MARGIN_X : 0;
const iconWidth = LEGEND_ICON_SIZE + LEGEND_MARGIN_X;
const ellipsisDotWidth = getTextWidth(ELLIPSIS_DOT_TEXT, font);
const widestTextWidth = getTextWidth(WIDEST_TEXT, font);
const maxTextCount = Math.floor((width - ellipsisDotWidth - checkboxWidth - iconWidth) / widestTextWidth);
return maxTextCount > 0 ? maxTextCount : 0;
}
function getViewLabelInfo(legendInfo, label, maxTextLength) {
var _a, _b;
const { checkboxVisible, useSpectrumLegend, font, legendOptions } = legendInfo;
let viewLabel = label;
const itemWidth = (_b = (_a = legendOptions) === null || _a === void 0 ? void 0 : _a.item) === null || _b === void 0 ? void 0 : _b.width;
const itemWidthWithFullText = getItemWidth(viewLabel, checkboxVisible, useSpectrumLegend, font);
if (isNumber(itemWidth) && isNumber(maxTextLength) && itemWidth < itemWidthWithFullText) {
viewLabel = `${label.slice(0, maxTextLength)}${ELLIPSIS_DOT_TEXT}`;
}
return { viewLabel, width: (itemWidth !== null && itemWidth !== void 0 ? itemWidth : itemWidthWithFullText) };
}
function getLegendLabelsInfo(series, legendInfo) {
const maxTextLengthWithEllipsis = getMaxTextLengthWithEllipsis(legendInfo);
return Object.keys(series).flatMap((type) => series[type].map(({ name, colorValue, visible }) => {
const label = colorValue ? colorValue : name;
const { width, viewLabel } = getViewLabelInfo(legendInfo, label, maxTextLengthWithEllipsis);
return {
label,
type,
checked: (visible !== null && visible !== void 0 ? visible : true),
viewLabel,
width,
};
}));
}
function getItemWidth(label, checkboxVisible, useSpectrumLegend, font) {
return ((useSpectrumLegend
? 0
: (checkboxVisible ? LEGEND_CHECKBOX_SIZE + LEGEND_MARGIN_X : 0) +
LEGEND_ICON_SIZE +
LEGEND_MARGIN_X) + getTextWidth(label, font));
}
function getLegendDataAppliedTheme(data, series) {
const colors = Object.values(series).reduce((acc, cur) => (cur && cur.colors ? [...acc, ...cur.colors] : acc), []);
return data.map((datum, idx) => (Object.assign(Object.assign({}, datum), { color: colors[idx] })));
}
function getLegendState(options, series) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
const useSpectrumLegend = (_c = (_b = (_a = options) === null || _a === void 0 ? void 0 : _a.series) === null || _b === void 0 ? void 0 : _b.useColorValue, (_c !== null && _c !== void 0 ? _c : !!series.heatmap));
const useScatterChartIcon = !!((_d = series) === null || _d === void 0 ? void 0 : _d.scatter);
const checkboxVisible = useSpectrumLegend
? false
: showCheckbox(options);
const defaultTheme = makeDefaultTheme(series, (_g = (_f = (_e = options) === null || _e === void 0 ? void 0 : _e.theme) === null || _f === void 0 ? void 0 : _f.chart) === null || _g === void 0 ? void 0 : _g.fontFamily);
const font = getTitleFontString(deepMergedCopy(defaultTheme.legend.label, Object.assign({}, (_j = (_h = options.theme) === null || _h === void 0 ? void 0 : _h.legend) === null || _j === void 0 ? void 0 : _j.label)));
const legendInfo = {
checkboxVisible,
font,
useSpectrumLegend,
legendOptions: options.legend,
};
const legendLabelsInfo = hasNestedPieSeries(series)
? getNestedPieLegendLabelsInfo(series, legendInfo)
: getLegendLabelsInfo(series, legendInfo);
const data = legendLabelsInfo.map(({ label, type, checked, width, viewLabel }) => ({
label,
active: true,
checked,
width,
iconType: getIconType(type),
chartType: type,
rowIndex: 0,
columnIndex: 0,
viewLabel,
}));
return {
useSpectrumLegend,
useScatterChartIcon,
data,
};
}
function getNextColumnRowIndex(params) {
const { verticalAlign, columnCount, rowCount, legendCount } = params;
let { rowIndex, columnIndex } = params;
if (verticalAlign) {
const maxLen = legendCount / rowCount;
if (maxLen - 1 > columnIndex) {
columnIndex += 1;
}
else {
rowIndex += 1;
columnIndex = 0;
}
}
else {
const maxLen = legendCount / columnCount;
if (maxLen - 1 > rowIndex) {
rowIndex += 1;
}
else {
columnIndex += 1;
rowIndex = 0;
}
}
return [rowIndex, columnIndex];
}
function setIndexToLegendData(legendData, rowCount, columnCount, legendCount, verticalAlign) {
let columnIndex = 0;
let rowIndex = 0;
legendData.forEach((datum) => {
datum.rowIndex = rowIndex;
datum.columnIndex = columnIndex;
[rowIndex, columnIndex] = getNextColumnRowIndex({
rowCount,
columnCount,
verticalAlign,
legendCount,
rowIndex,
columnIndex,
});
});
}
const legend = {
name: 'legend',
state: ({ options, series }) => {
return {
legend: getLegendState(options, series),
circleLegend: {},
};
},
action: {
initLegendState({ state, initStoreState }) {
extend(state.legend, getLegendState(initStoreState.options, initStoreState.series));
},
setLegendLayout({ state }) {
if (state.legend.useSpectrumLegend) {
this.dispatch('setSpectrumLegendLayout');
}
else {
this.dispatch('setNormalLegendLayout');
}
},
setSpectrumLegendLayout({ state }) {
const { legend: { data: legendData }, series, options, chart, theme, } = state;
const align = getLegendAlign(options);
const visible = showLegend(options, series);
const verticalAlign = isVerticalAlign(align);
const legendWidths = legendData.map(({ width }) => width);
const itemHeight = getLegendItemHeight(theme.legend.label.fontSize);
const width = getSpectrumLegendWidth(legendWidths, chart.width, verticalAlign);
const height = getSpectrumLegendHeight(itemHeight, chart.height, verticalAlign);
extend(state.legend, { visible, align, width, height });
},
setNormalLegendLayout({ state, initStoreState }) {
const { legend: { data: legendData }, series, options, chart, theme, } = state;
const align = getLegendAlign(options);
const visible = showLegend(options, series);
const checkbox = showCheckbox(options);
const initialWidth = Math.min(chart.width / 5, INITIAL_LEGEND_WIDTH);
const verticalAlign = isVerticalAlign(align);
const isNestedPieChart = hasNestedPieSeries(initStoreState.series);
const isScatterChart = !!series.scatter;
const isBubbleChart = !!series.bubble;
const circleLegendVisible = isBubbleChart
? showCircleLegend(options)
: false;
const legendWidths = legendData.map(({ width }) => width);
const itemHeight = getLegendItemHeight(theme.legend.label.fontSize);
const { legendWidth, legendHeight, rowCount, columnCount } = calculateLegendSize({
initialWidth,
legendWidths,
options,
verticalAlign,
visible,
checkbox,
chart,
itemHeight,
circleLegendVisible,
});
setIndexToLegendData(legendData, rowCount, columnCount, legendWidths.length, verticalAlign);
extend(state.legend, {
visible,
align,
showCheckbox: checkbox,
width: legendWidth,
height: legendHeight,
});
if (isBubbleChart && circleLegendVisible) {
this.dispatch('updateCircleLegendLayout', { legendWidth });
}
if (!isNestedPieChart && !isNoData(series)) {
this.dispatch('updateLegendColor');
}
if (isScatterChart) {
this.dispatch('updateLegendIcon');
}
},
updateCircleLegendLayout({ state }, { legendWidth }) {
const width = legendWidth === 0
? INITIAL_CIRCLE_LEGEND_WIDTH
: Math.min(legendWidth, INITIAL_CIRCLE_LEGEND_WIDTH);
const radius = Math.max((width - LEGEND_MARGIN_X) / 2, 0);
extend(state.circleLegend, { visible: true, width, radius });
},
setLegendActiveState({ state }, { name, active }) {
const { data } = state.legend;
const model = data.find(({ label }) => label === name);
model.active = active;
this.notify(state, 'legend');
},
setAllLegendActiveState({ state }, active) {
state.legend.data.forEach((datum) => {
datum.active = active;
});
this.notify(state, 'legend');
},
setLegendCheckedState({ state }, { name, checked }) {
const model = state.legend.data.find(({ label }) => label === name);
model.checked = checked;
this.notify(state, 'legend');
},
updateLegendColor({ state }) {
const { legend: legendData, series } = state;
const data = getLegendDataAppliedTheme(legendData.data, series);
extend(state.legend, { data });
},
updateLegendIcon({ state }) {
const { legend: legendData, series } = state;
const data = legendData.data.reduce((acc, cur) => {
var _a;
if (cur.chartType === 'scatter' && ((_a = series.scatter) === null || _a === void 0 ? void 0 : _a.data)) {
const model = series.scatter.data.find(({ name }) => name === cur.label);
const iconType = model ? model.iconType : cur.iconType;
return [...acc, Object.assign(Object.assign({}, cur), { iconType })];
}
return [...acc, cur];
}, []);
extend(state.legend, { data });
},
updateNestedPieChartLegend({ state }) {
const { legend: legendData, nestedPieSeries } = state;
extend(state.legend, {
data: getLegendDataAppliedTheme(legendData.data, nestedPieSeries),
});
},
},
observe: {
updateLegendLayout() {
this.dispatch('setLegendLayout');
},
},
};
export default legend;