devexpress-reporting
Version:
DevExpress Reporting provides the capability to develop a reporting application to create and customize reports.
395 lines (394 loc) • 19.8 kB
JavaScript
/**
* DevExpress HTML/JS Reporting (chart\_initializer.js)
* Version: 25.1.3
* Build date: Jun 26, 2025
* Copyright (c) 2012 - 2025 Developer Express Inc. ALL RIGHTS RESERVED
* License: https://www.devexpress.com/Support/EULAs/universal.xml
*/
import { Size } from '@devexpress/analytics-core/analytics-elements';
import { $dx, ActionLists, addDisposeCallback, appendStaticContextToRootViewModel, DragHelperContent, extend, FieldListProvider, staticContext } from '@devexpress/analytics-core/analytics-internal';
import { addCultureInfo, getLocalization, TabInfo, TabPanel, UndoEngine } from '@devexpress/analytics-core/analytics-utils';
import { getTemplate, ObjectProperties } from '@devexpress/analytics-core/analytics-widgets';
import * as ko from 'knockout';
import { ChartStructureTreeListController } from './internal/chartStructure/_controller';
import { ChartStructureObjectProvider } from './internal/chartStructure/_provider';
import { legendName } from './internal/meta/_axis';
import { createViewsArray, points, viewBindableSerializationInfo } from './internal/meta/_chart';
import { axisXName, axisYName, paneName } from './internal/meta/_view';
import { editorTemplates } from './internal/_editorTemplates';
import { ChartRequests } from './internal/_requests';
import { chartControlSerializationsInfo, ChartControlViewModel, fakeChartSerializationInfo } from './_control';
import { controlsFactory } from './_controlsFactory';
import { ChartControlSurface } from './_surface';
export const ActionId = {
Save: 'dxxcd-save'
};
const ChartDesignerElements = {
Main: 'dx-chart-middlePart',
Toolbar: 'dxcd-toolbar',
RightPanel: 'dx-right-panel-lightweight',
LeftPanel: 'dx-chart-leftPanel'
};
export function registerControls() {
controlsFactory.registerControl('ChartControl', {
info: chartControlSerializationsInfo,
surfaceType: ChartControlSurface,
type: ChartControlViewModel,
elementActionsTypes: [],
isContainer: true,
nonToolboxItem: true
});
}
let _chartLimitation = false;
export function _setChartLimitation(chartLimitation) {
_chartLimitation = chartLimitation;
viewBindableSerializationInfo.valuesArray = createViewsArray(chartLimitation);
}
function customizeDesignerActions(designerModel, nextCustomizer) {
const chart = designerModel.model;
return ((actions) => {
const save = {
id: ActionId.Save,
text: 'Save',
imageClassName: 'dxrd-image-save',
imageTemplateName: 'dxrd-svg-menu-save',
disabled: ko.observable(false),
visible: true,
hasSeparator: true,
hotKey: { ctrlKey: true, keyCode: 'S'.charCodeAt(0) },
clickAction: () => {
chart().save();
}
};
actions.splice(0, 0, save);
nextCustomizer && nextCustomizer(actions);
});
}
export function updateChartSurfaceContentSize(element, surfaceSize, rtl = false) {
const $element = $dx(element);
return () => {
const rightAreaWidth = $element.find('.dxrd-right-panel')[0]['offsetWidth'];
const leftAreaWidth = $element.find('.dx-chart-left-panel')[0]['offsetWidth'];
const otherWidth = rightAreaWidth + leftAreaWidth, surfaceWidth = $element.find('.dxcd-designer')[0]['offsetWidth'] - (otherWidth + 5);
$element.find('.dxrd-surface-wrapper').css({
'right': `${!rtl ? rightAreaWidth : leftAreaWidth}px`,
'left': `${rtl ? rightAreaWidth : leftAreaWidth}px`,
'width': `${surfaceWidth}px`
});
surfaceSize(surfaceWidth);
};
}
export function subscribeTreelistArray(chartStructureProvider, array, getPath, subscribeNewItem) {
return array.subscribe((args) => {
args.forEach((changeSet) => {
if (changeSet.status) {
let selectedPath = '';
const path = getPath();
if (changeSet.status === 'deleted') {
selectedPath = array().length === 0 ? path.join('.') : [].concat(path, '0').join('.');
}
else if (changeSet.status === 'added') {
selectedPath = [].concat(path, changeSet.index).join('.');
subscribeNewItem && subscribeNewItem(changeSet.value, array, path);
}
chartStructureProvider.selectedPath(selectedPath);
}
});
}, null, 'arrayChange');
}
export function getPropertyInfo(serializationsInfo, index, pathComponets) {
let info = serializationsInfo.filter((info) => { return info.propertyName === pathComponets[index]; })[0];
if (info && info.info && index++ < pathComponets.length)
info = getPropertyInfo(info.info, index, pathComponets);
return info;
}
function wrapSelectedElement(selectedElement, selectedElementInfo, displayName, propertyName = 'element') {
const object = {};
object[propertyName] = selectedElement;
object['owner'] = ko.unwrap(selectedElement)['owner'];
object['getInfo'] = () => {
return [extend({}, selectedElementInfo, { displayName: displayName, propertyName: propertyName })];
};
return object;
}
export function createChartStructure(chart, selectedItem, subscriptions, surface, undoEngine, dragdrophandler) {
const fakeChart = {
dataSource: chart.dataSource,
dataMember: chart.chart.dataContainer.dataMember,
seriesDataMember: chart.chart.dataContainer.seriesDataMember,
seriesTemplate: chart.chart.dataContainer.seriesTemplate,
series: chart.chart.dataContainer.series,
diagram: ko.computed(() => {
if (chart.chart.dataContainer.series().length > 0 || !!chart.chart.dataContainer.seriesDataMember()) {
return chart.chart.diagram();
}
return null;
}),
titles: chart.chart.titles,
backImage: chart.chart['backImage'],
legend: chart.chart['legend'],
legends: chart.chart.legends,
emptyChartText: chart.chart['emptyChartText'],
smallChartText: chart.chart['smallChartText'],
paletteName: chart.chart['paletteName'],
getInfo: () => {
return fakeChartSerializationInfo;
},
getPath: (propertyName) => { return chart.getPath(propertyName); },
isPropertyDisabled: (propertyName) => { return chart.isPropertyDisabled(propertyName); },
className: () => { return 'chart'; }
};
const chartStructureProvider = new ChartStructureObjectProvider(fakeChart, 'Chart', 'DevExpress.XtraReports.UI.XRChart');
const chartStructureTreeListController = new ChartStructureTreeListController(['chart', 'Chart', 'titles', 'legend', 'legends', 'series', 'diagram', 'indicators',
'defaultPane', 'panes', 'axisX', 'axisY', 'secondaryAxesX', 'secondaryAxesY', 'constantLines', 'scaleBreaks', 'strips', 'seriesTemplate', 'label', 'points'], ['chart', 'diagram', 'axisX', 'axisY', 'titles', 'indicators', 'legends', 'series', 'panes', 'secondaryAxesX', 'secondaryAxesY', 'seriesTemplate', 'constantLines', 'scaleBreaks', 'strips', 'SeriesViewModel', 'SecondaryAxisViewModel'], (newItem) => {
let selectedElement = chartStructureProvider.selectedMember();
if (newItem.data.specifics === 'points') {
selectedElement = wrapSelectedElement(ko.observableArray(selectedElement), points, newItem.data.displayName, 'points');
}
else if (Array.isArray(selectedElement)) {
selectedElement = wrapSelectedElement(selectedElement, { editor: editorTemplates.getEditor('collection') }, newItem.data.displayName);
}
selectedItem(selectedElement);
}, surface, undoEngine, dragdrophandler);
const chartStructure = {
itemsProvider: chartStructureProvider,
treeListController: chartStructureTreeListController,
expandRootItems: true,
selectedPath: chartStructureProvider.selectedPath
};
chartStructureProvider.selectedPath('Chart');
selectedItem(null);
subscriptions.push(fakeChart.diagram);
subscriptions.push(subscribeTreelistArray(chartStructureProvider, chart.chart.dataContainer.series, () => ['Chart', 'series']));
subscriptions.push(subscribeTreelistArray(chartStructureProvider, chart.chart.titles, () => ['Chart', 'titles']));
subscriptions.push(subscribeTreelistArray(chartStructureProvider, chart.chart.legends, () => ['Chart', 'legends']));
const diagramSubscriptions = [];
const subscribeDiagram = (diagram) => {
diagramSubscriptions.forEach((val) => val.dispose());
const axisCollectionNames = ['constantLines', 'scaleBreaks', 'strips'];
const subscribeAxis = (axis, array, path) => {
axisCollectionNames.forEach((propertyName) => {
diagramSubscriptions.push(subscribeTreelistArray(chartStructureProvider, axis[propertyName], () => [].concat(path, array.indexOf(axis), propertyName)));
});
};
['axisX', 'axisY'].forEach(propertyName => {
if (diagram[propertyName]) {
axisCollectionNames.forEach(innerPropertyName => {
if (diagram[propertyName][innerPropertyName]) {
diagramSubscriptions.push(subscribeTreelistArray(chartStructureProvider, diagram[propertyName][innerPropertyName], () => ['Chart', 'diagram', propertyName, innerPropertyName]));
}
});
}
});
['secondaryAxesX', 'secondaryAxesY'].forEach(propertyName => {
if (diagram[propertyName]) {
diagramSubscriptions.push(subscribeTreelistArray(chartStructureProvider, diagram[propertyName], () => ['Chart', 'diagram', propertyName], subscribeAxis));
const axis = ko.unwrap(diagram[propertyName]);
for (let i = 0; i < axis.length; i++) {
subscribeAxis(axis[i], axis, ['Chart', 'diagram', propertyName]);
}
}
});
if (diagram.panes) {
diagramSubscriptions.push(subscribeTreelistArray(chartStructureProvider, diagram.panes, () => ['Chart', 'diagram', 'panes']));
}
[].push.apply(subscriptions, diagramSubscriptions);
};
subscribeDiagram(chart.chart.diagram());
chart.chart.diagram.subscribe((newVal) => {
subscribeDiagram(newVal);
});
return chartStructure;
}
function createArrayOfNames(collectionProperty, defaultVal) {
return ko.computed(() => {
return (defaultVal && [defaultVal] || []).concat(collectionProperty().map(x => { return x.name(); }));
});
}
function createArrayOfDisplayNames(collectionProperty, defaultVal) {
return ko.computed(() => {
return (defaultVal && [defaultVal] || []).concat(collectionProperty().map(x => {
return {
value: x.name(),
displayValue: x.name()
};
}));
});
}
export function createChartDesigner(element, options, applyBindings = true) {
if (options.localization) {
addCultureInfo(options.localization);
}
options.callbacks.fieldLists = options.callbacks.fieldLists || ChartRequests.fieldListCallback;
registerControls();
const chartControlModel = ko.observable(), surface = ko.observable(), dataBindingsProvider = ko.observable(), size = new Size(options.data.width, options.data.height);
const chartStructure = ko.observable(null);
const chartSelectedItem = ko.observable(null);
let subscriptions = [];
const groups = ko.observableArray();
let chartSourceSubscription = null;
const undoModel = ko.observable({});
const initChartControlModel = (newModel) => {
surface() && surface()._disposables.forEach((item) => { item.dispose(); });
subscriptions.forEach((item) => item.dispose());
subscriptions = [];
if (newModel) {
groups(newModel.chart.dataContainer.series().map(x => x.view()['group'] || x['stackedGroup']).filter(x => !!x).map(x => x()));
chartControlModel(newModel);
undoModel(newModel);
surface() && surface()._disposables.forEach((item) => { item.dispose(); });
surface(new ChartControlSurface(newModel, ko.observable(1), size));
const fieldListProvider = ko.unwrap(options.fieldListProvider);
if (!!fieldListProvider) {
dataBindingsProvider(fieldListProvider);
}
else {
let _chartSources = options.data.dataSource && options.data.dataSource() ? [options.data.dataSource()] : [];
if (options.data.availableChartDataSources) {
_chartSources = options.data.availableChartDataSources().map((x) => {
return x.value;
});
}
const realChartSources = ko.observableArray(_chartSources);
dataBindingsProvider(new FieldListProvider(options.callbacks.fieldLists, realChartSources));
}
newModel.fieldListProvider(dataBindingsProvider());
designerModel.dragHelperContent = new DragHelperContent(null);
chartStructure(createChartStructure(newModel, chartSelectedItem, subscriptions, surface, designerModel.undoEngine, designerModel.dragHelperContent));
}
designerModel.undoEngine().clearHistory();
};
const init = (chartSourceValue) => {
const newModel = new ChartControlViewModel({
chartSource: chartSourceValue,
dataSource: options.data.dataSource,
callbacks: options.callbacks,
size: {
height: ko.observable(options.data.height),
width: ko.observable(options.data.width),
isPropertyDisabled: (name) => { return true; }
}
});
initChartControlModel(newModel);
};
const undoEngine = new UndoEngine(undoModel, ['viewType'], 'getInfo');
undoEngine['_disposeUndoEngineSubscriptionsName'] += 'chartdesigner';
const model = new ObjectProperties(chartSelectedItem);
model.registerAccessibilityProvider(options.accessibilityProvider);
const designerModel = {
model: chartControlModel,
chartStructure: chartStructure,
rightPanelHeader: () => getLocalization('Chart Structure', 'ChartDesignerStringIDs.ChartStructureDockPanelTitle'),
surface: surface,
undoEngine: ko.observable(undoEngine),
tabPanel: new TabPanel({
tabs: [
new TabInfo({
text: 'Properties',
template: 'dxrd-propertygridtab',
model: model,
localizationId: 'AnalyticsCoreStringId.Cmd_Properties'
})
],
rtl: options.rtl,
width: 396
}),
surfaceSize: ko.observable(0),
isLoading: ko.observable(true),
canAddItems: options.canAddItems,
rtl: options.rtl,
groups: groups,
applyGroup: (groupName) => {
if (groups().indexOf(groupName) === -1 && !!groupName) {
groups.push(groupName);
}
}
};
appendStaticContextToRootViewModel(designerModel, staticContext);
if (options.data.chartSource) {
chartSourceSubscription = options.data.chartSource.subscribe((newValue) => {
init(newValue);
});
init(options.data.chartSource());
}
else {
options.data.chart.subscribe((newVal) => {
initChartControlModel(newVal);
});
initChartControlModel(options.data.chart());
}
designerModel.panes = createArrayOfNames(() => {
const panes = chartControlModel().chart.diagram().panes;
return panes && panes() || [];
}, paneName.defaultVal);
designerModel.legends = createArrayOfDisplayNames(() => {
const legends = chartControlModel().chart.legends;
return legends && legends() || [];
}, { value: legendName.defaultVal, displayValue: getLocalization(legendName.defaultVal, 'ChartStringId.DefaultLegend') });
designerModel.axisX = createArrayOfNames(() => {
const axisX = chartControlModel().chart.diagram().secondaryAxesX;
return axisX && axisX() || [];
}, axisXName.defaultVal);
designerModel.axisY = createArrayOfNames(() => {
const axisY = chartControlModel().chart.diagram().secondaryAxesY;
return axisY && axisY() || [];
}, axisYName.defaultVal);
if (options.data.availableChartDataSources) {
designerModel.chartDataSources = options.data.availableChartDataSources;
}
else {
const displayedChartSources = options.data.dataSource && options.data.dataSource() ? [{ displayName: options.data.dataSource().name, value: options.data.dataSource() }] : [];
designerModel.chartDataSources = ko.observableArray(displayedChartSources);
}
designerModel.rootStyle = 'dxcd-designer dxd-back-primary';
designerModel.parts = [
{ id: ChartDesignerElements.Main, templateName: ChartDesignerElements.Main, model: designerModel },
{ id: ChartDesignerElements.Toolbar, templateName: ChartDesignerElements.Toolbar, model: designerModel },
{ id: ChartDesignerElements.RightPanel, templateName: ChartDesignerElements.RightPanel, model: designerModel },
{ id: ChartDesignerElements.LeftPanel, templateName: ChartDesignerElements.LeftPanel, model: designerModel }
];
designerModel.dataBindingsProvider = dataBindingsProvider;
designerModel.actionLists = new ActionLists(surface, null, designerModel.undoEngine, customizeDesignerActions(designerModel, options.callbacks.customizeActions));
designerModel.isLoading(false);
if (applyBindings) {
$dx(element).children().remove();
ko.applyBindings(designerModel, element);
}
const updateSurfaceContentSize_ = updateChartSurfaceContentSize(element, designerModel.surfaceSize, designerModel.rtl);
designerModel.tabPanel.events.on('widthChanged', args => {
updateSurfaceContentSize_();
});
designerModel.subscriptions = subscriptions;
designerModel.chartSourceSubscription = chartSourceSubscription;
designerModel.updateSurfaceSize = () => {
updateSurfaceContentSize_();
};
if (applyBindings) {
designerModel.updateSurfaceSize();
}
options.callbacks && options.callbacks.init && options.callbacks.init(designerModel);
return designerModel;
}
ko.bindingHandlers['dxChartDesigner'] = {
init: function (element, valueAccessor) {
const options = ko.unwrap(valueAccessor()) || {};
const templateHtml = getTemplate('dxrd-designer');
const $element = $dx(element).empty().append(templateHtml);
let designerModel = createChartDesigner($element[0], options, false);
ko.applyBindings(designerModel, $element.children()[0]);
const visibleSubscription = options.visible.subscribe((val) => {
if (val)
designerModel.updateSurfaceSize();
});
designerModel.updateSurfaceSize();
addDisposeCallback(element, function () {
designerModel.model() && designerModel.model()._disposables.forEach((item) => item.dispose());
designerModel.subscriptions.forEach((item) => item.dispose());
designerModel.chartSourceSubscription && designerModel.chartSourceSubscription.dispose();
designerModel.subscriptions = [];
visibleSubscription.dispose();
designerModel = null;
});
return { controlsDescendantBindings: true };
}
};