UNPKG

@progress/kendo-charts

Version:

Kendo UI platform-independent Charts library

727 lines (623 loc) 24.5 kB
const ActionTypes = Object.freeze({ seriesType: 0, stacked: 1, categoryAxisX: 2, valueAxisY: 3, seriesChange: 4, areaMarginLeft: 5, areaMarginRight: 6, areaMarginTop: 7, areaMarginBottom: 8, areaBackground: 9, titleText: 10, titleFontName: 11, titleFontSize: 12, titleColor: 13, subtitleText: 14, subtitleFontName: 15, subtitleFontSize: 16, subtitleColor: 17, seriesColor: 18, seriesLabel: 19, legendVisible: 20, legendFontName: 21, legendFontSize: 22, legendColor: 23, legendPosition: 24, categoryAxisTitleText: 25, categoryAxisTitleFontName: 26, categoryAxisTitleFontSize: 27, categoryAxisTitleColor: 28, categoryAxisLabelsFontName: 29, categoryAxisLabelsFontSize: 30, categoryAxisLabelsColor: 31, categoryAxisLabelsRotation: 32, categoryAxisReverseOrder: 33, valueAxisTitleText: 34, valueAxisTitleFontName: 35, valueAxisTitleFontSize: 36, valueAxisTitleColor: 37, valueAxisLabelsFormat: 38, valueAxisLabelsFontName: 39, valueAxisLabelsFontSize: 40, valueAxisLabelsColor: 41, valueAxisLabelsRotation: 42, }); const fontSizes = [ { text: "10", value: "10px" }, { text: "12", value: "12px" }, { text: "14", value: "14px" }, { text: "16", value: "16px" }, { text: "20", value: "20px" }, { text: "28", value: "28px" }, { text: "42", value: "42px" }, { text: "56", value: "56px" } ]; const titleSizeDefault = '20px'; const subtitleSizeDefault = '16px'; const labelSizeDefault = '12px'; const axisTitleSizeDefault = '16px'; const fontNames = [ { text: "Arial", value: "Arial, Helvetica, sans-serif", style: { fontFamily: "Arial, Helvetica, sans-serif" }, }, { text: "Courier New", value: "'Courier New', Courier, monospace", style: { fontFamily: "'Courier New', Courier, monospace" }, }, { text: "Georgia", value: "Georgia, serif", style: { fontFamily: "Georgia, serif" }, }, { text: "Impact", value: "Impact, Charcoal, sans-serif", style: { fontFamily: "Impact, Charcoal, sans-serif" }, }, { text: "Lucida Console", value: "'Lucida Console', Monaco, monospace", style: { fontFamily: "'Lucida Console', Monaco, monospace" }, }, { text: "Tahoma", value: "Tahoma, Geneva, sans-serif", style: { fontFamily: "Tahoma, Geneva, sans-serif" }, }, { text: "Times New Roman", value: "'Times New Roman', Times,serif", style: { fontFamily: "'Times New Roman', Times,serif" }, }, { text: "Trebuchet MS", value: "'Trebuchet MS', Helvetica, sans-serif", style: { fontFamily: "'Trebuchet MS', Helvetica, sans-serif" }, }, { text: "Verdana", value: "Verdana, Geneva, sans-serif", style: { fontFamily: "Verdana, Geneva, sans-serif" }, }, ]; const fontNameDefault = fontNames[0].value; const columnType = "column"; const barType = "bar"; const lineType = "line"; const pieType = "pie"; const scatterType = "scatter"; const categoricalTypes = [columnType, barType, lineType, scatterType]; const scatterSeries = { type: lineType, width: 0, }; function isCategorical(type) { return type && categoricalTypes.includes(type); } const categoryTypes = ["string", "date", "number"]; const valueTypes = ["number"]; const axesDefinitions = { bar: [ { axisType: "category", types: categoryTypes }, { axisType: "value", types: valueTypes }, ], column: [ { axisType: "category", types: categoryTypes }, { axisType: "value", types: valueTypes }, ], line: [ { axisType: "category", types: categoryTypes }, { axisType: "value", types: valueTypes }, ], pie: [ { axisType: "category", types: categoryTypes }, { axisType: "value", types: valueTypes, count: 1 }, ], scatter: [ { axisType: "category", types: categoryTypes }, { axisType: "value", types: valueTypes }, ], }; function getFont(font, size) { return `${size || ""} ${font || ""}`.trim(); } function parseFont(font) { const spaceIndex = (font || "").indexOf(" "); const size = font && font.substring(0, spaceIndex); const name = font && font.substring(spaceIndex + 1); return { size, name }; } const updateFontName = (fontName, defaultSize, currentFont) => { const { size } = parseFont(currentFont); return fontName ? getFont(fontName, size || defaultSize) : ""; }; const updateFontSize = (fontSize, defaultFontName, currentFont) => { const { name } = parseFont(currentFont); return fontSize ? getFont(name || defaultFontName, fontSize) : ""; }; const hasValue = (value) => value !== undefined && value !== null; const recordWithValues = (data) => { const result = structuredClone(data[0]); result.forEach((item, i) => { if (!hasValue(item.value)) { for (let index = 0; index < data.length; index++) { const value = data[index][i].value; if (hasValue(value)) { item.value = value; break; } } } }); return result; }; const getCategoryColumnIndex = (data, categoryDef) => { const candidates = []; const sampleRecord = recordWithValues(data); categoryDef.types.forEach((type) => { sampleRecord.forEach((item, i) => { if (typeof item.value === type) { candidates.push(i); } }); }); const result = candidates.findIndex((index) => { const values = data.map((record) => record[index].value); return new Set(values).size === values.length; }); return Math.max(result, 0); }; const getValueColumnIndexes = (data, valuesDef) => { const candidates = []; const sampleRecord = recordWithValues(data); valuesDef.forEach((def) => { def.types.forEach((type) => { sampleRecord.forEach((item, i) => { if (typeof item.value === type) { candidates.push(i); } }); }); }); return candidates; }; const emptyState = () => structuredClone({ columns: [], data: [], series: [], initialSeries: [], categoryAxis: [ { categories: [], labels: { visible: true, rotation: "auto" }, title: { text: '' } } ], valueAxis: [{ labels: { visible: true, rotation: 'auto' } }], area: { margin: { left: undefined, right: undefined, top: undefined, bottom: undefined, }, }, title: { text: '' }, subtitle: { text: '' }, stack: false, }); const categoryValueChartState = (data, seriesType, options) => { const state = emptyState(); state.seriesType = seriesType; state.data = data || []; state.legend = { visible: true, position: "bottom" }; const chartDef = axesDefinitions[seriesType]; if (!chartDef || !data.length) { return state; } const firstRecord = data[0].slice(); state.columns = data[0].map((i) => String(i.field)); const categoryDef = chartDef.find((def) => def.axisType === "category"); let catIndex = -1; if (categoryDef) { catIndex = options && options.categoryAxis ? state.columns.indexOf(options.categoryAxis) : getCategoryColumnIndex(data, categoryDef); } const valuesDef = chartDef.filter((def) => def.axisType === "value"); let valuesIndexes = getValueColumnIndexes(data, valuesDef); if (valuesIndexes.includes(catIndex)) { if (valuesIndexes.length > 1) { valuesIndexes = valuesIndexes.filter((index) => index !== catIndex); } else { catIndex = -1; } } const series = []; valuesIndexes.forEach((index) => { const valuesColumn = firstRecord[index]; const valuesResult = []; data.forEach((record) => { valuesResult.push(record[index].value); }); series.push(Object.assign({}, {name: valuesColumn.field, type: seriesType, data: valuesResult, stack: false, labels: { visible: false }}, (seriesType === scatterType ? scatterSeries : {}))); }); const categories = catIndex > -1 ? data.map((item) => String( hasValue(item[catIndex].value) ? item[catIndex].value : " " ) ) : []; if (series.length) { state.series = series.map((s, i) => (Object.assign({}, s, {id: i}))); state.initialSeries = structuredClone(state.series); } state.categoryAxis = [ { categories, labels: { visible: true, rotation: "auto" } }, ]; state.categoryField = state.columns[catIndex]; return state; }; const pieChartState = (data, seriesType, options) => { const state = emptyState(); state.seriesType = seriesType; state.data = data || []; const chartDef = axesDefinitions[seriesType]; if (!chartDef || !data.length) { return state; } const categoriesAxis = data[0].map((i) => i.field); const categoryDef = chartDef.find((def) => def.axisType === "category"); let catIndex = -1; if (categoryDef) { catIndex = options && options.categoryAxis ? categoriesAxis.indexOf(options.categoryAxis) : getCategoryColumnIndex(data, categoryDef); } const valuesDef = chartDef.filter((def) => def.axisType === "value"); let valuesIndexes = []; if (options && options.valueAxis) { valuesIndexes = [categoriesAxis.indexOf(options.valueAxis)]; } else { valuesIndexes = getValueColumnIndexes(data, valuesDef); } if (valuesIndexes.includes(catIndex) && valuesIndexes.length > 1) { valuesIndexes = valuesIndexes.filter((index) => index !== catIndex); } if (typeof valuesDef[0].count === "number") { valuesIndexes = valuesIndexes.slice(0, valuesDef[0].count); } const categories = catIndex > -1 ? data.map((item) => String(item[catIndex].value)) : []; const flatData = []; data.forEach((item) => { const record = {}; valuesIndexes.forEach((index) => { const col = item[index]; record[col.field] = col.value || 0; record[item[catIndex].field] = item[catIndex].value || " "; }); flatData.push(record); }); state.columns = categoriesAxis; state.categoryAxis = [{ categories, title: { text: "" } }]; state.series = [ { id: 0, data: flatData, type: seriesType, name: categoriesAxis[catIndex], labels: { visible: true }, categoryField: categoriesAxis[catIndex], field: categoriesAxis[valuesIndexes[0]], }, ]; state.categoryField = categoriesAxis[catIndex]; state.valueField = categoriesAxis[valuesIndexes[0]]; state.initialSeries = structuredClone(state.series); return state; }; function createInitialState(data, seriesType, defaultState) { const state = createState( data, (defaultState && defaultState.seriesType) || seriesType ); return typeof (defaultState && defaultState.stack) !== "undefined" ? updateState(state, ActionTypes.stacked, defaultState.stack) : state; } function createState(data, seriesType) { return (isCategorical(seriesType) ? categoryValueChartState : pieChartState)( data, seriesType ); } function mergeStates(source, target) { const newState = structuredClone(target); newState.legend = source.legend; newState.area = source.area; newState.title = source.title; newState.subtitle = source.subtitle; if (newState.series.length === source.series.length) { for (let i = 0; i < newState.series.length; i++) { newState.series[i].color = source.series[i].color; newState.series[i].labels = source.series[i].labels; } } if ( source.series.every((s) => s.labels && s.labels.visible) && isCategorical(newState.seriesType) && isCategorical(source.seriesType) ) { newState.series.forEach((s) => { s.labels = s.labels || {}; s.labels.visible = true; }); } return newState; } /* eslint-disable complexity */ function updateState(currentState, action, value) { const state = Object.assign({}, currentState); switch (action) { case ActionTypes.seriesType: return createState(state.data, value); case ActionTypes.stacked: state.series = state.series.map((s) => (Object.assign({}, s, {stack: value}))); state.stack = value; return state; case ActionTypes.categoryAxisX: { if (state.seriesType && isCategorical(state.seriesType)) { const newState = categoryValueChartState( state.data, state.seriesType, { categoryAxis: value } ); return mergeStates(state, newState); } else if (state.seriesType === pieType) { const newState = pieChartState(state.data, state.seriesType, { categoryAxis: value, }); return mergeStates(state, newState); } return state; } case ActionTypes.valueAxisY: { if (state.seriesType === pieType) { const newState = pieChartState(state.data, state.seriesType, { categoryAxis: state.categoryField, valueAxis: value, }); return mergeStates(state, newState); } return state; } case ActionTypes.seriesChange: state.series = value; return state; case ActionTypes.areaMarginLeft: state.area = Object.assign({}, state.area, {margin: Object.assign({}, ((state.area && state.area.margin) || {}), {left: value})}); return state; case ActionTypes.areaMarginRight: state.area = Object.assign({}, state.area, {margin: Object.assign({}, ((state.area && state.area.margin) || {}), {right: value})}); return state; case ActionTypes.areaMarginTop: state.area = Object.assign({}, state.area, {margin: Object.assign({}, ((state.area && state.area.margin) || {}), {top: value})}); return state; case ActionTypes.areaMarginBottom: state.area = Object.assign({}, state.area, {margin: Object.assign({}, ((state.area && state.area.margin) || {}), {bottom: value})}); return state; case ActionTypes.areaBackground: state.area = Object.assign({}, state.area, {background: value}); return state; case ActionTypes.titleText: state.title = Object.assign({}, state.title, {text: value}); return state; case ActionTypes.titleFontName: { state.title = Object.assign({}, state.title, {font: updateFontName( value, titleSizeDefault, state.title && state.title.font )}); return state; } case ActionTypes.titleFontSize: state.title = Object.assign({}, state.title, {font: updateFontSize( value, fontNameDefault, state.title && state.title.font )}); return state; case ActionTypes.titleColor: state.title = Object.assign({}, state.title, {color: value}); return state; case ActionTypes.subtitleText: state.subtitle = Object.assign({}, state.subtitle, {text: value}); return state; case ActionTypes.subtitleFontName: state.subtitle = Object.assign({}, state.subtitle, {font: updateFontName( value, subtitleSizeDefault, state.subtitle && state.subtitle.font )}); return state; case ActionTypes.subtitleFontSize: state.subtitle = Object.assign({}, state.subtitle, {font: updateFontSize( value, fontNameDefault, state.subtitle && state.subtitle.font )}); return state; case ActionTypes.subtitleColor: state.subtitle = Object.assign({}, state.subtitle, {color: value}); return state; case ActionTypes.seriesColor: state.series = state.series.map((s) => (Object.assign({}, s, {color: value.seriesName === s.name ? value.color : s.color}))); return state; case ActionTypes.seriesLabel: state.series = state.series.map((s) => { if (value.all || value.seriesName === s.name) { return Object.assign({}, s, {labels: { visible: value.visible }}); } return s; }); return state; case ActionTypes.legendVisible: state.legend = Object.assign({}, state.legend, {visible: value}); return state; case ActionTypes.legendFontName: { const legend = state.legend || {}; state.legend = Object.assign({}, legend, {labels: Object.assign({}, legend.labels, {font: updateFontName( value, labelSizeDefault, legend.labels && legend.labels.font )})}); return state; } case ActionTypes.legendFontSize: { const legend = state.legend || {}; state.legend = Object.assign({}, legend, {labels: Object.assign({}, legend.labels, {font: updateFontSize( value, fontNameDefault, legend.labels && legend.labels.font )})}); return state; } case ActionTypes.legendColor: { const legend = state.legend || {}; state.legend = Object.assign({}, legend, {labels: Object.assign({}, legend.labels, {color: value})}); return state; } case ActionTypes.legendPosition: state.legend = Object.assign({}, state.legend, {position: value}); return state; case ActionTypes.categoryAxisTitleText: state.categoryAxis = (state.categoryAxis || []).map(axis => (Object.assign({}, axis, {title: Object.assign({}, axis.title, {text: value})}))); return state; case ActionTypes.categoryAxisTitleFontName: { state.categoryAxis = (state.categoryAxis || []).map(axis => (Object.assign({}, axis, {title: Object.assign({}, axis.title, {font: updateFontName(value, axisTitleSizeDefault, axis.title && axis.title.font)})}))); return state; } case ActionTypes.categoryAxisTitleFontSize: state.categoryAxis = (state.categoryAxis || []).map(axis => (Object.assign({}, axis, {title: Object.assign({}, axis.title, {font: updateFontSize(value, fontNameDefault, axis.title && axis.title.font)})}))); return state; case ActionTypes.categoryAxisTitleColor: state.categoryAxis = (state.categoryAxis || []).map(axis => (Object.assign({}, axis, {title: Object.assign({}, axis.title, {color: value})}))); return state; case ActionTypes.categoryAxisLabelsFontName: { state.categoryAxis = (state.categoryAxis || []).map(axis => (Object.assign({}, axis, {labels: Object.assign({}, axis.labels, {font: updateFontName(value, labelSizeDefault, axis.labels && axis.labels.font)})}))); return state; } case ActionTypes.categoryAxisLabelsFontSize: state.categoryAxis = (state.categoryAxis || []).map(axis => (Object.assign({}, axis, {labels: Object.assign({}, axis.labels, {font: updateFontSize(value, fontNameDefault, axis.labels && axis.labels.font)})}))); return state; case ActionTypes.categoryAxisLabelsColor: state.categoryAxis = (state.categoryAxis || []).map(axis => (Object.assign({}, axis, {labels: Object.assign({}, axis.labels, {color: value})}))); return state; case ActionTypes.categoryAxisLabelsRotation: { const rotation = hasValue(value) ? value : 'auto'; state.categoryAxis = (state.categoryAxis || []).map(axis => (Object.assign({}, axis, {labels: Object.assign({}, axis.labels, {rotation})}))); return state; } case ActionTypes.categoryAxisReverseOrder: state.categoryAxis = (state.categoryAxis || []).map(axis => (Object.assign({}, axis, {reverse: value}))); return state; case ActionTypes.valueAxisTitleText: { if (!state.valueAxis || state.valueAxis.length === 0) { state.valueAxis = [{ title: { text: value } }]; } else { state.valueAxis = (state.valueAxis || []).map(axis => (Object.assign({}, axis, {title: Object.assign({}, axis.title, {text: value})}))); } return state; } case ActionTypes.valueAxisTitleFontName: { state.valueAxis = (state.valueAxis || []).map(axis => (Object.assign({}, axis, {title: Object.assign({}, axis.title, {font: updateFontName(value, axisTitleSizeDefault, axis.title && axis.title.font)})}))); return state; } case ActionTypes.valueAxisTitleFontSize: state.valueAxis = (state.valueAxis || []).map(axis => (Object.assign({}, axis, {title: Object.assign({}, axis.title, {font: updateFontSize(value, fontNameDefault, axis.title && axis.title.font)})}))); return state; case ActionTypes.valueAxisTitleColor: state.valueAxis = (state.valueAxis || []).map(axis => (Object.assign({}, axis, {title: Object.assign({}, axis.title, {color: value})}))); return state; case ActionTypes.valueAxisLabelsFormat: state.valueAxis = (state.valueAxis || []).map(axis => (Object.assign({}, axis, {labels: Object.assign({}, axis.labels, {format: value})}))); return state; case ActionTypes.valueAxisLabelsFontName: { state.valueAxis = (state.valueAxis || []).map(axis => (Object.assign({}, axis, {labels: Object.assign({}, axis.labels, {font: updateFontName(value, labelSizeDefault, axis.labels && axis.labels.font)})}))); return state; } case ActionTypes.valueAxisLabelsFontSize: state.valueAxis = (state.valueAxis || []).map(axis => (Object.assign({}, axis, {labels: Object.assign({}, axis.labels, {font: updateFontSize(value, fontNameDefault, axis.labels && axis.labels.font)})}))); return state; case ActionTypes.valueAxisLabelsColor: state.valueAxis = (state.valueAxis || []).map(axis => (Object.assign({}, axis, {labels: Object.assign({}, axis.labels, {color: value})}))); return state; case ActionTypes.valueAxisLabelsRotation: { const rotation = hasValue(value) ? value : 'auto'; state.valueAxis = (state.valueAxis || []).map(axis => (Object.assign({}, axis, {labels: Object.assign({}, axis.labels, {rotation: rotation})}))); return state; } default: return state; } } export { ActionTypes, fontSizes, fontNames, isCategorical, parseFont, createInitialState, createState, mergeStates, updateState };