UNPKG

survey-analytics

Version:

SurveyJS analytics Library.

1,234 lines (1,221 loc) 408 kB
/*! * surveyjs - SurveyJS Dashboard library v2.3.7 * Copyright (c) 2015-2025 Devsoft Baltic OÜ - http://surveyjs.io/ * License: MIT (http://www.opensource.org/licenses/mit-license.php) */ import { D as DocumentHelper, l as localization, g as createLoadingIndicator, b as DataHelper, e as createCommercialLicenseLink, f as svgTemplate, t as toPrecision } from './shared.mjs'; import { Event, QuestionCustomModel, QuestionCompositeModel, hasLicense, QuestionCommentModel, settings, ItemValue, surveyLocalization, IsTouch } from 'survey-core'; /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; class DataProvider { constructor(_data = []) { this._data = _data; this.filterValues = {}; /** * Fires when data has been changed. */ this.onDataChanged = new Event(); } get data() { if (Array.isArray(this._data)) { return this._data; } return undefined; } set data(data) { if (Array.isArray(data)) { this._data = [].concat(data); } else { this._data = data; } this.raiseDataChanged(); } get dataFn() { if (typeof this._data === "function") { return this._data; } return undefined; } get filteredData() { if (this._filteredData === undefined) { let filterKeys = Object.keys(this.filterValues); if (filterKeys.length > 0) { this._filteredData = this.data.filter((item) => { return !Object.keys(this.filterValues).some((key) => { const filterValue = this.filterValues[key]; const filterValueType = typeof filterValue; const questionValue = item[key]; if (Array.isArray(questionValue)) { if (filterValueType !== "object") return questionValue.indexOf(filterValue) == -1; } if (typeof questionValue === "object") { if (filterValueType !== "object") return true; return !questionContainsValue(questionValue, filterValue); } const seriesValue = item[DataProvider.seriesMarkerKey]; if (!!seriesValue && filterValueType === "object") { return questionValue !== filterValue[seriesValue]; } if (filterValueType === "object" && filterValue.start !== undefined && filterValue.end !== undefined) { let continioiusValue = typeof questionValue === "number" ? questionValue : Date.parse(questionValue); if (isNaN(continioiusValue)) { continioiusValue = parseFloat(questionValue); if (isNaN(continioiusValue)) { return true; } } return continioiusValue < filterValue.start || continioiusValue >= filterValue.end; } return item[key] !== this.filterValues[key]; }); }); } else { this._filteredData = this.data; } } return this._filteredData; } /** * Sets filter by question name and value. */ setFilter(questionName, selectedValue) { var filterChanged = true; if (selectedValue !== undefined) { filterChanged = this.filterValues[questionName] !== selectedValue; this.filterValues[questionName] = selectedValue; } else { filterChanged = this.filterValues[questionName] !== undefined; delete this.filterValues[questionName]; } if (filterChanged) { this.raiseDataChanged(); } } raiseDataChanged(questionName) { this._filteredData = undefined; if (!this.onDataChanged.isEmpty) { this.onDataChanged.fire(this, { questionName }); } } getFilters() { return Object.keys(this.filterValues).map(key => ({ field: key, type: "=", value: this.filterValues[key] })); } } DataProvider.seriesMarkerKey = "__sa_series_name"; function questionContainsValue(questionValue, filterValue) { const questionValueKeys = Object.keys(questionValue); const filterValueKeys = Object.keys(filterValue); if (filterValueKeys.length > questionValueKeys.length) return false; for (var key of filterValueKeys) { if (filterValue[key] != questionValue[key]) return false; } return true; } /** * An object with methods used to register and unregister visualizers for individual question types. * * [View Demo](https://surveyjs.io/dashboard/examples/custom-survey-data-visualizer/ (linkStyle)) */ class VisualizationManager { /** * Registers a visualizer for a specified question type. * * [View Demo](https://surveyjs.io/dashboard/examples/custom-survey-data-visualizer/ (linkStyle)) * @param questionType A question [type](https://surveyjs.io/form-library/documentation/api-reference/question#getType). * @param constructor A function that returns a visualizer constructor to register. * @param index A zero-based index that specifies the visualizer's position in the visualizer list for the specified question type. Pass `0` to insert the visualizer at the beginning of the list and use it by default. If `index` is not specified, the visualizer is added to the end of the list. */ static registerVisualizer(questionType, constructor, index = Number.MAX_VALUE) { let visualizers = VisualizationManager.vizualizers[questionType]; if (!visualizers) { visualizers = []; VisualizationManager.vizualizers[questionType] = visualizers; } visualizers.push({ ctor: constructor, index }); } /** * Unregisters a visualizer for a specified question type. * * [View Demo](https://surveyjs.io/dashboard/examples/visualize-answers-from-text-entry-fields-with-charts/ (linkStyle)) * @param questionType A question [type](https://surveyjs.io/form-library/documentation/api-reference/question#getType). * @param constructor A function that returns a visualizer constructor to unregister. */ static unregisterVisualizer(questionType, constructor) { let questionTypes = [questionType]; if (!questionType) { questionTypes = Object.keys(VisualizationManager.vizualizers); } questionTypes.forEach(qType => { if (constructor) { let visualizers = VisualizationManager.vizualizers[qType]; if (!!visualizers) { const vDescr = visualizers.filter(v => v.ctor === constructor)[0]; if (!!vDescr) { let index = visualizers.indexOf(vDescr); if (index !== -1) { visualizers.splice(index, 1); } } } } else { VisualizationManager.vizualizers[qType] = []; } }); } /** * @deprecated Call the [`unregisterVisualizer()`](https://surveyjs.io/dashboard/documentation/api-reference/visualizationmanager#unregisterVisualizer) method instead. * @param constructor A function that returns a visualizer constructor to unregister. */ static unregisterVisualizerForAll(constructor) { VisualizationManager.unregisterVisualizer(undefined, constructor); } /** * Returns all visualizer constructors for a specified question type. * @param visualizerType A question [type](https://surveyjs.io/form-library/documentation/api-reference/question#getType). */ static getVisualizersByType(visualizerType, fallbackVisualizerType) { let vDescrs = VisualizationManager.vizualizers[visualizerType]; if (!!fallbackVisualizerType && (!vDescrs || vDescrs.length == 0)) { vDescrs = VisualizationManager.vizualizers[fallbackVisualizerType]; } if (!vDescrs) { if (VisualizationManager.defaultVisualizer.suppressVisualizerStubRendering) { return []; } return [VisualizationManager.defaultVisualizer]; } vDescrs = [].concat(vDescrs); vDescrs.sort((v1, v2) => v1.index - v2.index); return vDescrs.map(v => v.ctor); } /** * Returns a constructor for an alternative visualizer selector. * @see registerAltVisualizerSelector */ static getAltVisualizerSelector() { return VisualizationManager.alternativesVisualizer || VisualizationManager.defaultVisualizer; } /** * Registers an alternative visualizer selector. * @param constructor A function that returns a constructor for an alternative visualizer selector. */ static registerAltVisualizerSelector(constructor) { VisualizationManager.alternativesVisualizer = constructor; } static getPivotVisualizerConstructor() { return VisualizationManager.pivotVisualizer || VisualizationManager.defaultVisualizer; } static registerPivotVisualizer(constructor) { VisualizationManager.pivotVisualizer = constructor; } } VisualizationManager.defaultVisualizer = undefined; VisualizationManager.alternativesVisualizer = undefined; VisualizationManager.pivotVisualizer = undefined; VisualizationManager.vizualizers = {}; /** * An object that allows you to create individual visualizers without creating a [visualization panel](https://surveyjs.io/dashboard/documentation/api-reference/visualizationpanel). */ class VisualizerFactory { /** * Creates a visualizer for a single question. * * ```js * import { VisualizerFactory } from "survey-analytics"; * * const visualizer = new VisualizerFactory.createVisualizer( * question, * data, * options * ); * * visualizer.render("containerId") * ``` * * If a question has more than one [registered](https://surveyjs.io/dashboard/documentation/api-reference/visualizationmanager#registerVisualizer) visualizer, users can switch between them using a drop-down menu. * @param question A question for which to create a visualizer. * @param data A data array with survey results to be visualized. * @param options An object with any custom properties you need within the visualizer. */ static createVisualizer(question, data, options) { let type = question.getType(); let creators = []; let questionForCreator = question; let optionsForCreator = Object.assign({}, options); if (type === "text" && question.inputType) { creators = VisualizationManager.getVisualizersByType(question.inputType, type); } else { let fallbackType = undefined; if (question instanceof QuestionCustomModel) { fallbackType = question.getDynamicType(); // questionForCreator = question.contentQuestion; } else if (question instanceof QuestionCompositeModel) { fallbackType = "composite"; } creators = VisualizationManager.getVisualizersByType(type, fallbackType); } var visualizers = creators.map((creator) => new creator(questionForCreator, data, optionsForCreator)); if (visualizers.length > 1) { const alternativesVisualizerConstructor = VisualizationManager.getAltVisualizerSelector(); let visualizer = new alternativesVisualizerConstructor(visualizers, questionForCreator, data, optionsForCreator); return visualizer; } return visualizers[0]; } } function defaultStatisticsCalculator(data, dataInfo) { const dataNames = dataInfo.dataNames; const statistics = []; const values = dataInfo.getValues(); const valuesIndex = {}; values.forEach((val, index) => { valuesIndex[val] = index; }); const processMissingAnswers = values.indexOf(undefined) !== -1; const series = dataInfo.getSeriesValues(); const seriesIndex = {}; series.forEach((val, index) => { seriesIndex[val] = index; }); const seriesLength = series.length || 1; for (var i = 0; i < dataNames.length; ++i) { const dataNameStatistics = new Array(); for (var j = 0; j < seriesLength; ++j) { dataNameStatistics.push(new Array(values.length).fill(0)); } statistics.push(dataNameStatistics); } const getValueIndex = (val) => { if (val !== null && typeof val === "object") return valuesIndex[val.value]; return valuesIndex[val]; }; data.forEach((row) => { dataNames.forEach((dataName, index) => { const rowValue = row[dataName]; if (rowValue !== undefined || processMissingAnswers) { const rowValues = Array.isArray(rowValue) ? rowValue : [rowValue]; if (series.length > 0) { const rowName = row[DataProvider.seriesMarkerKey]; if (rowName !== undefined) { // Series are labelled by seriesMarkerKey in row data const seriesNo = seriesIndex[rowName] || 0; rowValues.forEach((val) => { const valIndex = getValueIndex(val); statistics[index][seriesNo][valIndex]++; }); } else { // Series are the keys in question value (matrix question) // TODO: think about the de-normalization and combine with the previous case rowValues.forEach((val) => { series.forEach((seriesName) => { if (val[seriesName] !== undefined) { const seriesNo = seriesIndex[seriesName] || 0; const values = Array.isArray(val[seriesName]) ? val[seriesName] : [val[seriesName]]; values.forEach(value => { const valIndex = getValueIndex(value); statistics[index][seriesNo][valIndex]++; }); } }); }); } } else { // No series rowValues.forEach((val) => { const valIndex = getValueIndex(val); statistics[0][0][valIndex]++; }); } } }); }); return dataInfo.dataNames.length > 1 ? statistics : statistics[0]; } function histogramStatisticsCalculator(data, intervals, seriesValues) { const statistics = []; if (seriesValues.length === 0) { seriesValues.push(""); } for (var i = 0; i < seriesValues.length; ++i) { statistics.push(intervals.map(i => 0)); data[seriesValues[i]].forEach(dataValue => { for (let j = 0; j < intervals.length; ++j) { if (intervals[j].start <= dataValue && (dataValue < intervals[j].end || j == intervals.length - 1)) { statistics[i][j]++; break; } } }); } return statistics; } function mathStatisticsCalculator(data, dataName) { let resultMin = Number.MAX_VALUE, resultMax = -Number.MAX_VALUE, resultAverage = 0; let actualAnswerCount = 0; data.forEach((rowData) => { if (rowData[dataName] !== undefined) { const questionValue = +rowData[dataName]; actualAnswerCount++; resultAverage += questionValue; if (resultMin > questionValue) { resultMin = questionValue; } if (resultMax < questionValue) { resultMax = questionValue; } } }); if (actualAnswerCount > 0) { resultAverage = resultAverage / actualAnswerCount; } resultAverage = Math.ceil(resultAverage * 100) / 100; return [resultAverage, resultMin, resultMax]; } class PostponeHelper { static postpone(fn, timeout) { if (PostponeHelper.postponeFunction) { return PostponeHelper.postponeFunction(fn, timeout); } else { return setTimeout(fn, timeout); } } } /** * A base object for all visualizers. Use it to implement a custom visualizer. * * Constructor parameters: * * - `question`: [`Question`](https://surveyjs.io/form-library/documentation/api-reference/question)\ * A survey question to visualize. * - `data`: `Array<any>`\ * Survey results. * - `options`\ * An object with the following properties: * - `dataProvider`: `DataProvider`\ * A data provider for this visualizer. * - `renderContent`: `(contentContainer: HTMLElement, visualizer: VisualizerBase) => void`\ * A function that renders the visualizer's HTML markup. Append the markup to `contentContainer`. * - `survey`: [`SurveyModel`](https://surveyjs.io/form-library/documentation/api-reference/survey-data-model)\ * Pass a `SurveyModel` instance if you want to use locales from the survey JSON schema. * - `seriesValues`: `Array<string>`\ * Series values used to group data. * - `seriesLabels`: `Array<string>`\ * Series labels to display. If this property is not set, `seriesValues` are used as labels. * - `type`: `string`\ * *(Optional)* The visualizer's type. * * [View Demo](https://surveyjs.io/dashboard/examples/how-to-plot-survey-data-in-custom-bar-chart/ (linkStyle)) */ class VisualizerBase { afterRender(contentContainer) { this.onAfterRender.fire(this, { htmlElement: contentContainer }); } stateChanged(name, value) { if (this._settingState) { return; } this.onStateChanged.fire(this, this.getState()); } getToolbarItemCreators() { return Object.assign({}, this.toolbarItemCreators, this.onGetToolbarItemCreators && this.onGetToolbarItemCreators() || {}); } constructor(question, data, options = {}, _type) { var _a; this.question = question; this.options = options; this._type = _type; this._showToolbar = true; this._footerVisualizer = undefined; this._dataProvider = undefined; this._getDataCore = undefined; this.labelTruncateLength = 27; this.haveCommercialLicense = false; this.renderResult = undefined; this.toolbarContainer = undefined; this.headerContainer = undefined; this.contentContainer = undefined; this.footerContainer = undefined; this._supportSelection = false; this._chartAdapter = undefined; this._footerIsCollapsed = undefined; /** * An event that is raised after the visualizer's content is rendered. * * Parameters: * * - `sender`: `VisualizerBase`\ * A `VisualizerBase` instance that raised the event. * * - `options.htmlElement`: `HTMLElement`\ * A page element with the visualizer's content. * @see render * @see refresh **/ this.onAfterRender = new Event(); /** * An event that is raised after a new locale is set. * * Parameters: * * - `sender`: `VisualizerBase`\ * A `VisualizerBase` instance that raised the event. * * - `options.locale`: `string`\ * The indentifier of a new locale (for example, "en"). * @see locale */ this.onLocaleChanged = new Event(); // public onStateChanged = new Event< // (sender: VisualizationPanel, state: IState) => any, // VisualizationPanel, // any // >(); /** * An event that is raised when the visualizer's state has changed. * * The state includes selected chart types, chart layout, sorting, filtering, and other customizations that a user has made while using the dashboard. Handle the `onStateChanged` event to save these customizations, for example, in `localStorage` and restore them when the user reloads the page. * * Parameters: * * - `sender`: `VisualizerBase`\ * A `VisualizerBase` instance that raised the event. * * - `state`: `any`\ * A new state of the visualizer. Includes information about the visualized elements and current locale. * * [View Demo](https://surveyjs.io/dashboard/examples/save-dashboard-state-to-local-storage/ (linkStyle)) * @see getState * @see setState */ this.onStateChanged = new Event(); this.toolbarItemCreators = {}; this._backgroundColor = "#f7f7f7"; this._calculationsCache = undefined; this.loadingData = false; this._settingState = false; const f = hasLicense; this.haveCommercialLicense = (!!f && f(4)) || VisualizerBase.haveCommercialLicense || (typeof options.haveCommercialLicense !== "undefined" ? options.haveCommercialLicense : false); this._getDataCore = (_a = this.questionOptions) === null || _a === void 0 ? void 0 : _a.getDataCore; this._dataProvider = options.dataProvider || new DataProvider(data); this._dataProvider.onDataChanged.add(() => this.onDataChanged()); this.loadingData = !!this._dataProvider.dataFn; if (typeof options.labelTruncateLength !== "undefined") { this.labelTruncateLength = options.labelTruncateLength; } } get questionOptions() { var _a; return this.options[(_a = this.question) === null || _a === void 0 ? void 0 : _a.name]; } onDataChanged() { this._calculationsCache = undefined; this.loadingData = !!this._dataProvider.dataFn; this.refresh(); } /** * Returns the identifier of a visualized question. */ get name() { return this.question.valueName || this.question.name; } get dataNames() { return [this.name]; } /** * Indicates whether the visualizer displays a header. This property is `true` when a visualized question has a correct answer. * @see hasFooter */ get hasHeader() { if (!this.options || !this.options.showCorrectAnswers) { return false; } return !!this.question && !!this.question.correctAnswer; } /** * Indicates whether the visualizer displays a footer. This property is `true` when a visualized question has a comment. * @see hasHeader */ get hasFooter() { return (!!this.question && (this.question.hasComment || this.question.hasOther)); } createVisualizer(question, options, data) { let visualizerOptions = Object.assign({}, options || this.options); if (visualizerOptions.dataProvider === undefined) { visualizerOptions.dataProvider = this.dataProvider; } return VisualizerFactory.createVisualizer(question, data || this.data, visualizerOptions); } /** * Allows you to access the footer visualizer. Returns `undefined` if the footer is absent. * @see hasFooter */ get footerVisualizer() { if (!this.hasFooter) { return undefined; } if (!this._footerVisualizer) { const question = new QuestionCommentModel(this.question.name + (settings || {}).commentPrefix); question.title = this.processText(this.question.title); let visualizerOptions = Object.assign({}, this.options); visualizerOptions.renderContent = undefined; this._footerVisualizer = this.createVisualizer(question, visualizerOptions); if (!!this._footerVisualizer) { this._footerVisualizer.onUpdate = () => this.invokeOnUpdate(); } } return this._footerVisualizer; } /** * Indicates whether users can select series points to cross-filter charts. To allow or disallow selection, set the [`allowSelection`](https://surveyjs.io/dashboard/documentation/api-reference/ivisualizationpaneloptions#allowSelection) property of the `IVisualizationPanelOptions` object in the [`VisualizationPanel`](https://surveyjs.io/dashboard/documentation/api-reference/visualizationpanel) constructor. */ get supportSelection() { return (this.options.allowSelection === undefined || this.options.allowSelection) && this._supportSelection; } getSeriesValues() { return this.options.seriesValues || []; } getSeriesLabels() { return this.options.seriesLabels || this.getSeriesValues(); } getValues() { throw new Error("Method not implemented."); } getLabels() { return this.getValues(); } /** * Registers a function used to create a toolbar item for this visualizer. * * The following code shows how to add a custom button and drop-down menu to the toolbar: * * ```js * import { VisualizationPanel, DocumentHelper } from "survey-analytics"; * * const vizPanel = new VisualizationPanel( ... ); * * // Add a custom button to the toolbar * vizPanel.visualizers[0].registerToolbarItem("my-toolbar-button", () => { * return DocumentHelper.createButton( * // A button click event handler * () => { * alert("Custom toolbar button is clicked"); * }, * // Button caption * "Button" * ); * }); * * // Add a custom drop-down menu to the toolbar * vizPanel.visualizers[0].registerToolbarItem("my-toolbar-dropdown", () => { * return DocumentHelper.createSelector( * // Menu items * [ * { value: 1, text: "One" }, * { value: 2, text: "Two" }, * { value: 3, text: "Three" } * ], * // A function that specifies initial selection * (option) => false, * // An event handler that is executed when selection is changed * (e) => { * alert(e.target.value); * } * ); * }); * ``` * @param name A custom name for the toolbar item. * @param creator A function that accepts the toolbar and should return an `HTMLElement` with the toolbar item. * @see unregisterToolbarItem */ registerToolbarItem(name, creator, order = 100) { this.toolbarItemCreators[name] = { creator, order }; } /** * * Unregisters a function used to create a toolbar item. Allows you to remove a toolbar item. * @param name A toolbar item name. * @returns A function previously used to [register](#registerToolbarItem) the removed toolbar item. * @see registerToolbarItem */ unregisterToolbarItem(name) { if (this.toolbarItemCreators[name] !== undefined) { const item = this.toolbarItemCreators[name]; delete this.toolbarItemCreators[name]; return item.creator; } return undefined; } /** * Returns the visualizer's type. */ get type() { return this._type || "visualizer"; } /** * @deprecated Use [`surveyData`](https://surveyjs.io/dashboard/documentation/api-reference/visualizationpanel#surveyData) instead. */ get data() { return this.dataProvider.filteredData; } /** * Returns an array of survey results used to calculate values for visualization. If a user applies a filter, the array is also filtered. * * To get an array of calculated and visualized values, call the [`getCalculatedValues()`](https://surveyjs.io/dashboard/documentation/api-reference/visualizerbase#getCalculatedValues) method. */ get surveyData() { return this.dataProvider.filteredData; } get dataProvider() { return this._dataProvider; } /** * Updates visualized data. * @param data A data array with survey results to be visualized. */ updateData(data) { const dataPath = this.options.dataPath; let dataToAssign = data; if (!!dataPath && Array.isArray(data)) { dataToAssign = []; data.forEach(dataItem => { if (!!dataItem && dataItem[dataPath] !== undefined) { if (Array.isArray(dataItem[dataPath])) { dataToAssign = dataToAssign.concat(dataItem[dataPath]); } else { dataToAssign.push(dataItem[dataPath]); } } }); } if (!this.options.dataProvider) { this.dataProvider.data = dataToAssign; } if (this.hasFooter) { this.footerVisualizer.updateData(dataToAssign); } } invokeOnUpdate() { this.onUpdate && this.onUpdate(); } /** * Deletes the visualizer and all its elements from the DOM. * @see clear */ destroy() { if (!!this.renderResult) { this.clear(); this.toolbarContainer = undefined; this.headerContainer = undefined; this.contentContainer = undefined; this.footerContainer = undefined; this.renderResult.innerHTML = ""; this.renderResult = undefined; } if (!!this._footerVisualizer) { this._footerVisualizer.destroy(); this._footerVisualizer.onUpdate = undefined; this._footerVisualizer = undefined; } } /** * Empties the toolbar, header, footer, and content containers. * * If you want to empty and delete the visualizer and all its elements from the DOM, call the [`destroy()`](https://surveyjs.io/dashboard/documentation/api-reference/visualizerbase#destroy) method instead. */ clear() { if (!!this.toolbarContainer) { this.destroyToolbar(this.toolbarContainer); } if (!!this.headerContainer) { this.destroyHeader(this.headerContainer); } if (!!this.contentContainer) { this.destroyContent(this.contentContainer); } if (!!this.footerContainer) { this.destroyFooter(this.footerContainer); } } createToolbarItems(toolbar) { const toolbarItemCreators = this.getToolbarItemCreators(); const sortedItems = Object.keys(toolbarItemCreators || {}) .map(name => (Object.assign({ name }, toolbarItemCreators[name]))) .sort((a, b) => a.order - b.order); sortedItems.forEach((item) => { let toolbarItem = item.creator(toolbar); if (!!toolbarItem) { toolbar.appendChild(toolbarItem); } }); } getCorrectAnswerText() { return !!this.question ? this.question.correctAnswer : ""; } destroyToolbar(container) { container.innerHTML = ""; } renderToolbar(container) { if (this.showToolbar) { const toolbar = (DocumentHelper.createElement("div", "sa-toolbar")); this.createToolbarItems(toolbar); container.appendChild(toolbar); } } destroyHeader(container) { if (!!this.options && typeof this.options.destroyHeader === "function") { this.options.destroyHeader(container, this); } else { container.innerHTML = ""; } } destroyContent(container) { if (!!this.options && typeof this.options.destroyContent === "function") { this.options.destroyContent(container, this); } else if (this._chartAdapter) { this._chartAdapter.destroy(container.children[0]); } container.innerHTML = ""; } renderHeader(container) { if (!!this.options && typeof this.options.renderHeader === "function") { this.options.renderHeader(container, this); } else { const correctAnswerElement = DocumentHelper.createElement("div", "sa-visualizer__correct-answer"); correctAnswerElement.innerText = localization.getString("correctAnswer") + this.getCorrectAnswerText(); container.appendChild(correctAnswerElement); } } renderContentAsync(container) { return __awaiter(this, void 0, void 0, function* () { if (this._chartAdapter) { const chartNode = DocumentHelper.createElement("div"); container.innerHTML = ""; container.appendChild(chartNode); yield this._chartAdapter.create(chartNode); } else { container.innerText = localization.getString("noVisualizerForQuestion"); } return container; }); } ensureQuestionIsReady() { return new Promise((resolve) => { if (this.question) { this.question.waitForQuestionIsReady().then(() => resolve()); } else { resolve(); } }); } renderContent(container) { if (!!this.options && typeof this.options.renderContent === "function") { const rendered = this.options.renderContent(container, this); if (rendered !== false) { this.afterRender(container); } } else { if (this.loadingData) { this.renderLoadingIndicator(this.contentContainer); } this.ensureQuestionIsReady().then(() => this.renderContentAsync(container).then(el => this.afterRender(el))); } } destroyFooter(container) { container.innerHTML = ""; } get isFooterCollapsed() { if (this._footerIsCollapsed === undefined) { this._footerIsCollapsed = VisualizerBase.otherCommentCollapsed; } return this._footerIsCollapsed; } set isFooterCollapsed(newVal) { this._footerIsCollapsed = newVal; } renderFooter(container) { container.innerHTML = ""; if (this.hasFooter) { const footerTitleElement = DocumentHelper.createElement("h4", "sa-visualizer__footer-title", { innerText: localization.getString("otherCommentTitle") }); container.appendChild(footerTitleElement); const footerContentElement = DocumentHelper.createElement("div", "sa-visualizer__footer-content"); footerContentElement.style.display = this.isFooterCollapsed ? "none" : "block"; const visibilityButtonText = localization.getString(this.isFooterCollapsed ? "showButton" : "hideButton"); const visibilityButton = DocumentHelper.createButton(() => { if (footerContentElement.style.display === "none") { footerContentElement.style.display = "block"; visibilityButton.innerText = localization.getString("hideButton"); this._footerIsCollapsed = false; } else { footerContentElement.style.display = "none"; visibilityButton.innerText = localization.getString("showButton"); this._footerIsCollapsed = true; } this.footerVisualizer.invokeOnUpdate(); }, visibilityButtonText /*, "sa-toolbar__button--right"*/); container.appendChild(visibilityButton); container.appendChild(footerContentElement); this.footerVisualizer.render(footerContentElement); } } /** * Renders the visualizer in a specified container. * @param targetElement An `HTMLElement` or an `id` of a page element in which you want to render the visualizer. */ render(targetElement) { if (typeof targetElement === "string") { targetElement = document.getElementById(targetElement); } this.renderResult = targetElement; this.toolbarContainer = DocumentHelper.createElement("div", "sa-visualizer__toolbar"); targetElement.appendChild(this.toolbarContainer); this.renderToolbar(this.toolbarContainer); if (this.hasHeader) { this.headerContainer = DocumentHelper.createElement("div", "sa-visualizer__header"); targetElement.appendChild(this.headerContainer); this.renderHeader(this.headerContainer); } this.contentContainer = DocumentHelper.createElement("div", "sa-visualizer__content"); targetElement.appendChild(this.contentContainer); this.renderContent(this.contentContainer); this.footerContainer = DocumentHelper.createElement("div", "sa-visualizer__footer"); targetElement.appendChild(this.footerContainer); this.renderFooter(this.footerContainer); } updateToolbar() { if (!!this.toolbarContainer) { PostponeHelper.postpone(() => { this.destroyToolbar(this.toolbarContainer); this.renderToolbar(this.toolbarContainer); }); } } isSupportSoftUpdateContent() { return false; } softUpdateContent() { } hardUpdateContent() { this.destroyContent(this.contentContainer); this.renderContent(this.contentContainer); } updateContent() { if (!this.isSupportSoftUpdateContent()) { this.hardUpdateContent(); } else { this.softUpdateContent(); } } /** * Re-renders the visualizer and its content. */ refresh() { if (!!this.headerContainer) { PostponeHelper.postpone(() => { this.destroyHeader(this.headerContainer); this.renderHeader(this.headerContainer); this.invokeOnUpdate(); }); } if (!!this.contentContainer) { PostponeHelper.postpone(() => { this.updateContent(); this.invokeOnUpdate(); }); } if (!!this.footerContainer) { PostponeHelper.postpone(() => { this.destroyFooter(this.footerContainer); this.renderFooter(this.footerContainer); this.invokeOnUpdate(); }); } } processText(text) { if (this.options.stripHtmlFromTitles !== false) { let originalText = text || ""; let processedText = originalText.replace(/(<([^>]+)>)/gi, ""); return processedText; } return text; } getRandomColor() { const colors = this.getColors(); return colors[Math.floor(Math.random() * colors.length)]; } get backgroundColor() { return this.getBackgroundColorCore(); } set backgroundColor(value) { this.setBackgroundColorCore(value); } getBackgroundColorCore() { return this._backgroundColor; } setBackgroundColorCore(color) { this._backgroundColor = color; if (this.footerVisualizer) this.footerVisualizer.backgroundColor = color; } getColors(count = 10) { const colors = Array.isArray(VisualizerBase.customColors) && VisualizerBase.customColors.length > 0 ? VisualizerBase.customColors : VisualizerBase.colors; let manyColors = []; for (let index = 0; index < count; index++) { manyColors = manyColors.concat(colors); } return manyColors; } /** * Gets or sets the visibility of the visualizer's toolbar. * * Default value: `true` */ get showToolbar() { return this._showToolbar; } set showToolbar(newValue) { if (newValue != this._showToolbar) { this._showToolbar = newValue; if (!!this.toolbarContainer) { this.destroyToolbar(this.toolbarContainer); this.renderToolbar(this.toolbarContainer); } } } /** * @deprecated Use [`getCalculatedValues()`](https://surveyjs.io/dashboard/documentation/api-reference/visualizationpanel#getCalculatedValues) instead. */ getData() { return this.getCalculatedValuesCore(); } getCalculatedValuesCore() { if (!!this._getDataCore) { return this._getDataCore(this); } return defaultStatisticsCalculator(this.surveyData, this); } renderLoadingIndicator(contentContainer) { contentContainer.appendChild(createLoadingIndicator()); } convertFromExternalData(externalCalculatedData) { return externalCalculatedData; } /** * Returns an array of calculated and visualized values. If a user applies a filter, the array is also filtered. * * To get an array of source survey results, use the [`surveyData`](https://surveyjs.io/dashboard/documentation/api-reference/visualizerbase#surveyData) property. */ getCalculatedValues() { return new Promise((resolve, reject) => { if (this._calculationsCache !== undefined) { resolve(this._calculationsCache); } if (!!this.dataProvider.dataFn) { this.loadingData = true; const dataLoadingPromise = this.dataProvider.dataFn({ visualizer: this, filter: this.dataProvider.getFilters(), callback: (loadedData) => { this.loadingData = false; if (!loadedData.error && Array.isArray(loadedData.data)) { this._calculationsCache = this.convertFromExternalData(loadedData.data); resolve(this._calculationsCache); } else { reject(); } } }); if (dataLoadingPromise) { dataLoadingPromise .then(calculatedData => { this.loadingData = false; this._calculationsCache = this.convertFromExternalData(calculatedData); resolve(this._calculationsCache); }) .catch(() => { this.loadingData = false; reject(); }); } } else { this._calculationsCache = this.getCalculatedValuesCore(); resolve(this._calculationsCache); } }); } /** * Returns an object with properties that describe a current visualizer state. The properties are different for each individual visualizer. * * > This method is overriden in classes descendant from `VisualizerBase`. * @see setState * @see resetState * @see onStateChanged */ getState() { return {}; } /** * Sets the visualizer's state. * * [View Demo](https://surveyjs.io/dashboard/examples/save-dashboard-state-to-local-storage/ (linkStyle)) * * > This method is overriden in classes descendant from `VisualizerBase`. * @see getState * @see resetState * @see onStateChanged */ setState(state) { } /** * Resets the visualizer's state. * * > This method is overriden in classes descendant from `VisualizerBase`. * @see getState * @see setState * @see onStateChanged */ resetState() { } /** * Gets or sets the current locale. * * If you want to inherit the locale from a visualized survey, assign a [`SurveyModel`](https://surveyjs.io/form-library/documentation/api-reference/survey-data-model) instance to the [`survey`](https://surveyjs.io/dashboard/documentation/api-reference/ivisualizationpaneloptions#survey) property of the `IVisualizationPanelOptions` object in the [`VisualizationPanel`](https://surveyjs.io/dashboard/documentation/api-reference/visualizationpanel) constructor. * * If the survey is [translated into more than one language](https://surveyjs.io/form-library/examples/survey-localization/), the toolbar displays a language selection drop-down menu. * * [View Demo](https://surveyjs.io/dashboard/examples/localize-survey-data-dashboard-ui/ (linkStyle)) * @see onLocaleChanged */ get locale() { var survey = this.options.survey; if (!!survey) { return survey.locale; } return localization.currentLocale; } set locale(newLocale) { this.setLocale(newLocale); this.onLocaleChanged.fire(this, { locale: newLocale }); this.refresh(); } setLocale(newLocale) { localization.currentLocale = newLocale; var survey = this.options.survey; if (!!survey && survey.locale !== newLocale) { survey.locale = newLocale; } } } VisualizerBase.haveCommercialLicense = false; VisualizerBase.suppressVisualizerStubRendering = false; VisualizerBase.chartAdapterType = undefined; // public static otherCommentQuestionType = "comment"; // TODO: make it configureable - allow choose what kind of question/visualizer will be used for comments/others VisualizerBase.otherCommentCollapsed = true; VisualizerBase.customColors = []; VisualizerBase.colors = [ "#86e1fb", "#3999fb", "#ff6771", "#1eb496", "#ffc152", "#aba1ff", "#7d8da5", "#4ec46c", "#cf37a6", "#4e6198", ]; VisualizationManager.defaultVisualizer = VisualizerBase; class NumberModel extends VisualizerBase { constructor(question, data, options = {}, name) { super(question, data, options, name || "number"); if (VisualizerBase.chartAdapterType) { this._chartAdapter = new VisualizerBase.chartAdapterType(this); this.chartTypes = this._chartAdapter.getChartTypes(); this.chartType = this.chartTypes[0]; } this.registerToolbarItem("changeChartType", () => { if (this.chartTypes.length > 1) { return DocumentHelper.createSelector(this.chartTypes.map((chartType) => { return { value: chartType, text: localization.getString("chartType_" + chartType), }; }), (option) => this.chartType === option.value, (e) => { this.setChartType(e.target.value); }); } return null; }); } onDataChanged() { this._resultAverage = undefined; this._resultMin = undefined; this._resultMax = undefined; super.onDataChanged(); } onChartTypeChanged() { } setChartType(chartType) { if (this.chartTypes.indexOf(chartType) !== -1 && this.chartType !== chartType) { this.chartType = chartType; this.onChartTypeChanged(); if (!!this.contentContainer) { this.destroyContent(this.contentContainer); this.renderContent(this.contentContainer); } this.invokeOnUpdate(); } } destroy() { this._resultAverage = undefine