@progress/kendo-charts
Version:
Kendo UI platform-independent Charts library
727 lines (623 loc) • 24.5 kB
JavaScript
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
};