survey-analytics
Version:
SurveyJS analytics Library.
1,236 lines (1,223 loc) • 411 kB
JavaScript
/*!
* surveyjs - SurveyJS Dashboard library v2.0.4
* 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, c as createLoadingIndicator, a as DataHelper, s as svgTemplate, b as createCommercialLicenseLink, t as toPrecision } from './shared.mjs';
export { d as surveyStrings } from './shared.mjs';
import { Event, QuestionCommentModel, settings, ItemValue, hasLicense, surveyLocalization, IsTouch, Helpers } from 'survey-core';
import Plotly from 'plotly.js-dist-min';
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;
}
/******************************************************************************
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 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());
}
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.renderResult = undefined;
this.toolbarContainer = undefined;
this.headerContainer = undefined;
this.contentContainer = undefined;
this.footerContainer = undefined;
this._supportSelection = false;
/**
* 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](/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;
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) {
let visualizerOptions = Object.assign({}, options || this.options);
if (visualizerOptions.dataProvider === undefined) {
visualizerOptions.dataProvider = this.dataProvider;
}
return VisualizerFactory.createVisualizer(question, 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) {
this.toolbarItemCreators[name] = creator;
}
/**
*
* 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 creator = this.toolbarItemCreators[name];
delete this.toolbarItemCreators[name];
return 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) {
if (!this.options.dataProvider) {
this.dataProvider.data = data;
}
if (this.hasFooter) {
this.footerVisualizer.updateData(data);
}
}
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) {
Object.keys(this.toolbarItemCreators || {}).forEach((toolbarItemName) => {
let toolbarItem = this.toolbarItemCreators[toolbarItemName](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 {
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* () {
return new Promise((resolve, reject) => {
container.innerText = localization.getString("noVisualizerForQuestion");
resolve(container);
});
});
}
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.renderContentAsync(container).then(el => this.afterRender(el));
}
}
destroyFooter(container) {
container.innerHTML = "";
}
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 = VisualizerBase.otherCommentCollapsed
? "none"
: "block";
const visibilityButton = DocumentHelper.createButton(() => {
if (footerContentElement.style.display === "none") {
footerContentElement.style.display = "block";
visibilityButton.innerText = localization.getString("hideButton");
}
else {
footerContentElement.style.display = "none";
visibilityButton.innerText = localization.getString(VisualizerBase.otherCommentCollapsed ? "showButton" : "hideButton");
}
this.footerVisualizer.invokeOnUpdate();
}, localization.getString("showButton") /*, "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);
}
updateContent() {
this.destroyContent(this.contentContainer);
this.renderContent(this.contentContainer);
}
/**
* 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 onStateChanged
*/
getState() {
return {};
}
/**
* Sets the visualizer's state.
*
* > This method is overriden in classes descendant from `VisualizerBase`.
* @see getState
* @see onStateChanged
*/
setState(state) {
}
/**
* 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.
* @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.suppressVisualizerStubRendering = false;
// 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",
];
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);
}
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) {
if (row[DataProvider.seriesMarkerKey] !== undefined) {
// Series are labelled by seriesMarkerKey in row data
const seriesNo = seriesIndex[row[DataProvider.seriesMarkerKey]] || 0;
rowValues.forEach((val) => {
statistics[index][seriesNo][valuesIndex[val]]++;
});
}
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;
statistics[index][seriesNo][valuesIndex[val[seriesName]]]++;
}
});
});
}
}
else {
// No series
rowValues.forEach((val) => statistics[0][0][valuesIndex[val]]++);
}
}
});
});
return dataInfo.dataNames.length > 1 ? statistics : statistics[0];
}
/**
* An object with methods used to register and unregister visualizers for individual question types.
*
* [View Demo](https://surveyjs.io/dashboard/examples/visualize-answers-from-text-entry-fields-with-charts/ (linkStyle))
*/
class VisualizationManager {
/**
* Registers 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 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 questionType A question [type](https://surveyjs.io/form-library/documentation/api-reference/question#getType).
*/
static getVisualizersByType(questionType) {
let vDescrs = VisualizationManager.vizualizers[questionType];
if (!vDescrs) {
if (VisualizerBase.suppressVisualizerStubRendering) {
return [];
}
return [VisualizerBase];
}
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 || VisualizerBase;
}
/**
* Registers an alternative visualizer selector.
* @param constructor A function that returns a constructor for an alternative visualizer selector.
*/
static registerAltVisualizerSelector(constructor) {
VisualizationManager.alternativesVisualizer = constructor;
}
}
VisualizationManager.alternativesVisualizer = 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;
if (question.getType() === "text" && question.inputType) {
type = question.inputType;
}
else {
type = question.getType();
}
var creators = VisualizationManager.getVisualizersByType(type);
var visualizers = creators.map((creator) => new creator(question, data, options));
if (visualizers.length > 1) {
const alternativesVisualizerConstructor = VisualizationManager.getAltVisualizerSelector();
let visualizer = new alternativesVisualizerConstructor(visualizers, question, data, options);
return visualizer;
}
return visualizers[0];
}
}
class NumberModel extends VisualizerBase {
constructor(question, data, options = {}, name) {
super(question, data, options, name || "number");
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 = undefined;
this._resultMin = undefined;
this._resultMax = undefined;
super.destroy();
}
generateText(maxValue, minValue, stepsCount) {
let texts = [];
if (stepsCount === 5) {
texts = [
"very high (" + maxValue + ")",
"high",
"medium",
"low",
"very low (" + minValue + ")",
];
}
else {
texts.push(maxValue);
for (let i = 0; i < stepsCount - 2; i++) {
texts.push("");
}
texts.push(minValue);
}
if (!!NumberModel.generateTextsCallback) {
return NumberModel.generateTextsCallback(this.question, maxValue, minValue, stepsCount, texts);
}
return texts;
}
generateValues(maxValue, stepsCount) {
const values = [];
for (let i = 0; i < stepsCount; i++) {
values.push(maxValue / stepsCount);
}
values.push(maxValue);
return values;
}
generateColors(maxValue, minValue, stepsCount) {
const palette = this.getColors();
const colors = [];
for (let i = 0; i < stepsCount; i++) {
colors.push(palette[i]);
}
colors.push("rgba(255, 255, 255, 0)");
return colors;
}
convertFromExternalData(externalCalculatedData) {
return [externalCalculatedData.value || 0, externalCalculatedData.minValue || 0, externalCalculatedData.maxValue || 0];
}
getCalculatedValuesCore() {
if (this._resultAverage === undefined ||
this._resultMin === undefined ||
this._resultMax === undefined) {
this._resultMin = Number.MAX_VALUE;
this._resultMax = -Number.MAX_VALUE;
this._resultAverage = 0;
let actualAnswerCount = 0;
this.data.forEach((rowData) => {
if (rowData[this.question.name] !== undefined) {
const questionValue = +rowData[this.question.name];
actualAnswerCount++;
this._resultAverage += questionValue;
if (this._resultMin > questionValue) {
this._resultMin = questionValue;
}
if (this._resultMax < questionValue) {
this._resultMax = questionValue;
}
}
});
if (actualAnswerCount > 0) {
this._resultAverage = this._resultAverage / actualAnswerCount;
}
this._resultAverage = Math.ceil(this._resultAverage * 100) / 100;
}
return [this._resultAverage, this._resultMin, this._resultMax];
}
}
NumberModel.stepsCount = 5;
NumberModel.showAsPercentage = false;
function hideEmptyAnswersInData(answersData) {
const result = {
datasets: [],
labels: [],
colors: [],
texts: [],
seriesLabels: [],
};
if (answersData.seriesLabels.length === 0) {
result.datasets.push([]);
result.texts.push([]);
for (var i = 0; i < answersData.datasets[0].length; i++) {
if (answersData.datasets[0][i] != 0) {
result.datasets[0].push(answersData.datasets[0][i]);
result.labels.push(answersData.labels[i]);
result.colors.push(answersData.colors[i]);
result.texts[0].push(answersData.texts[0][i]);
}
}
return result;
}
const seriesDataExistence = [];
seriesDataExistence.length = answersData.seriesLabels.length;
const valuesDataExistence = [];
valuesDataExistence.length = answersData.labels.length;
for (var valueIndex = 0; valueIndex < answersData.labels.length; valueIndex++) {
for (var seriesIndex = 0; seriesIndex < answersData.seriesLabels.length; seriesIndex++) {
if (answersData.datasets[valueIndex][seriesIndex] != 0) {
seriesDataExistence[seriesIndex] = true;
valuesDataExistence[valueIndex] = true;
}
}
}
for (var valueIndex = 0; valueIndex < valuesDataExistence.length; valueIndex++) {
if (valuesDataExistence[valueIndex]) {
result.labels.push(answersData.labels[valueIndex]);
result.colors.push(answersData.colors[valueIndex]);
}
}
for (var seriesIndex = 0; seriesIndex < answersData.seriesLabels.length; seriesIndex++) {
if (seriesDataExistence[seriesIndex]) {
result.seriesLabels.push(answersData.seriesLabels[seriesIndex]);
}
}
for (var valueIndex = 0; valueIndex < answersData.labels.length; valueIndex++) {
if (valuesDataExistence[valueIndex]) {
const dataset = [];
const texts = [];
for (var seriesIndex = 0; seriesIndex < answersData.datasets.length; seriesIndex++) {
if (seriesDataExistence[seriesIndex]) {
dataset.push(answersData.datasets[valueIndex][seriesIndex]);
texts.push(answersData.texts[valueIndex][seriesIndex]);
}
}
result.datasets.push(dataset);
result.texts.push(texts);
}
}
return result;
}
class SelectBase extends VisualizerBase {
constructor(question, data, options, name) {
super(question, data, options, name || "selectBase");
this.selectedItem = undefined;
this.choicesOrderSelector = undefined;
this.showPercentageBtn = undefined;
this.emptyAnswersBtn = undefined;
this.transposeDataBtn = undefined;
this.topNSelector = undefined;
this._showPercentages = false;
this._showOnlyPercentages = false;
this._percentagePrecision = 0;
this._answersOrder = "default";
this._supportSelection = true;
this._hideEmptyAnswers = false;
this._topN = -1;
this.topNValues = [].concat(SelectBase.topNValuesDefaults);
this._transposeData = false;
this._showMissingAnswers = false;
this.missingAnswersBtn = undefined;
this.chartTypes = [];
this._chartType = "bar";
/**
* Fires when answer data has been combined before they passed to draw graph.
* options - the answers data object containing: datasets, labels, colors, additional texts (percentage).
* options fields can be modified
*/
this.onAnswersDataReady = new Event();
question.visibleChoicesChangedCallback = () => {
this.dataProvider.raiseDataChanged();
};
this._showPercentages = this.options.showPercentages === true;
this._showOnlyPercentages = this.options.showOnlyPercentages === true;
if (this.options.percentagePrecision) {
this._percentagePrecision = this.options.percentagePrecision;
}
this._hideEmptyAnswers = this.options.hideEmptyAnswers === true;
this._answersOrder = this.options.a