UNPKG

survey-analytics

Version:

SurveyJS analytics Library.

740 lines (733 loc) 29.5 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 { Event, ItemValue } from 'survey-core'; import { l as localization, b as DataHelper } from './shared.mjs'; export { D as DocumentHelper, a as setupLocale, s as surveyStrings } from './shared.mjs'; import { N as NumberModel, b as VisualizationManager, S as SelectBase, r as defaultStatisticsCalculator, B as BooleanModel, H as HistogramModel, M as Matrix, q as PivotModel, R as RankingModel, a as VisualizerBase, _ as __awaiter } from './shared2.mjs'; export { A as AlternativeVisualizersWrapper, D as DataProvider, o as NpsAdapter, p as NpsVisualizer, n as NpsVisualizerWidget, P as PostponeHelper, l as StatisticsTable, k as StatisticsTableAdapter, m as StatisticsTableBoolean, j as Text, T as TextTableAdapter, g as VisualizationComposite, f as VisualizationMatrixDropdown, e as VisualizationMatrixDynamic, c as VisualizationPanel, d as VisualizationPanelDynamic, V as VisualizerFactory, i as WordCloud, W as WordCloudAdapter, h as hideEmptyAnswersInData, t as textHelper } from './shared2.mjs'; import Plotly from 'plotly.js-dist-min'; class PlotlySetup { static setup(charType, model, answersData) { return this.setups[charType](model, answersData); } static setupPie(model, answersData) { let { datasets, labels, colors, texts, seriesLabels, } = answersData; const hasSeries = seriesLabels.length > 1 || model.question.getType() === "matrix"; const layoutColumns = 2; let traces = []; const traceConfig = { type: model.chartType, labels: labels, customdata: labels, text: labels.map((label) => { return PlotlySetup.getTruncatedLabel(label, model.labelTruncateLength); }), hoverinfo: "value+text", textposition: "inside", texttemplate: "%{text}" }; if (model.chartType === "doughnut") { traceConfig.type = "pie"; traceConfig.hole = 0.4; } if (!hasSeries) { traceConfig.mode = "markers", traceConfig.marker = { color: colors }; traceConfig.marker.symbol = "circle"; traceConfig.marker.size = 16; } datasets.forEach((dataset, index) => { const isNotEmpty = dataset.some((value) => value != 0); let pieTexts = traceConfig.text; if (model.showPercentages) { const percentages = model.getPercentages([dataset])[0]; pieTexts = labels.map((l, li) => (model.showOnlyPercentages ? percentages[li] : PlotlySetup.getTruncatedLabel(l, model.labelTruncateLength) + "<br>" + percentages[li]) + "%"); } if (isNotEmpty) { traces.push(Object.assign({}, traceConfig, { values: dataset, text: pieTexts, domain: { column: traces.length % layoutColumns, row: Math.floor(traces.length / layoutColumns), }, title: { position: "bottom center", text: seriesLabels[index] } })); } }); const radius = labels.length < 10 ? labels.length * 50 + 100 : 550; const height = (radius + 25) * Math.ceil(traces.length / layoutColumns); const layout = { font: { family: "Segoe UI, sans-serif", size: 14, weight: "normal", color: "#404040", }, height: height, margin: { l: 0, t: 25, b: 0, r: 10, }, colorway: colors, hovermode: "closest", plot_bgcolor: model.backgroundColor, paper_bgcolor: model.backgroundColor, showlegend: false, }; if (hasSeries) { layout.annotations = []; layout.grid = { rows: Math.ceil(traces.length / layoutColumns), columns: layoutColumns }; } return { traces, layout, hasSeries }; } static setupBar(model, answersData) { let lineHeight = 30; let topMargin = 30; let bottomMargin = 30; let { datasets, labels, colors, texts, seriesLabels, } = answersData; const hasSeries = seriesLabels.length > 1 || model.question.getType() === "matrix"; const traces = []; const traceConfig = { type: model.chartType === "line" ? "line" : "bar", y: labels, customdata: labels, hoverinfo: "text", orientation: "h", textposition: "none", }; if (!hasSeries) { traceConfig.width = 0.5; traceConfig.bargap = 0.5; traceConfig.mode = "markers", traceConfig.marker = { color: colors }; } datasets.forEach((dataset, index) => { const traceName = hasSeries ? seriesLabels[index] : labels[index]; const percentString = model.showPercentages ? "%" : ""; const trace = Object.assign({}, traceConfig, { x: dataset, name: traceName, width: hasSeries && model.chartType !== "stackedbar" ? 0.5 / seriesLabels.length : 0.5, text: texts[index], hovertext: labels.map((label, labelIndex) => { if (model.showOnlyPercentages) { return `${texts[index][labelIndex]}${percentString}`; } else { return hasSeries ? `${traceName} : ${label}, ${texts[index][labelIndex]}${percentString}` : `${texts[index][labelIndex]}${percentString}, ${label}`; } }), }); if (model.showPercentages) { let texttemplate = model.showOnlyPercentages ? "%{text}%" : "%{value} (%{text}%)"; trace.textposition = "inside"; trace.texttemplate = texttemplate; trace.width = hasSeries && model.chartType !== "stackedbar" ? 0.7 / seriesLabels.length : 0.9; trace.bargap = hasSeries && model.chartType !== "stackedbar" ? 0.3 / seriesLabels.length : 0.1; } traces.push(trace); }); const height = (labels.length + 1) * lineHeight + topMargin + bottomMargin; const layout = { font: { family: "Segoe UI, sans-serif", size: 14, weight: "normal", color: "#404040", }, height: height, margin: { t: topMargin, b: bottomMargin, r: 10, }, colorway: colors, hovermode: "closest", plot_bgcolor: model.backgroundColor, paper_bgcolor: model.backgroundColor, showlegend: hasSeries, barmode: hasSeries && model.chartType == "stackedbar" ? "stack" : "group", xaxis: { rangemode: "nonnegative", automargin: true, }, yaxis: { automargin: true, type: "category", orientation: "h", tickmode: "array", tickvals: labels, ticktext: labels.map((label) => { return PlotlySetup.getTruncatedLabel(label, model.labelTruncateLength) + " "; }), }, }; if (hasSeries && model.chartType !== "stackedbar") { layout.height = (labels.length * seriesLabels.length + 1) * lineHeight + topMargin + bottomMargin; } if (["ar", "fa"].indexOf(localization.currentLocale) !== -1) { layout.xaxis.autorange = "reversed"; layout.yaxis.side = "right"; layout.legend = { x: 0, y: 1, xanchor: "left", yanchor: "top" }; } return { traces, layout, hasSeries }; } static setupVBar(model, answersData) { let topMargin = 30; let bottomMargin = 30; let { datasets, labels, colors, texts, seriesLabels, } = answersData; const hasSeries = seriesLabels.length > 1 || model.question.getType() === "matrix"; if (model.type !== "histogram" && model.type !== "pivot") { labels = [].concat(labels).reverse(); seriesLabels = [].concat(seriesLabels).reverse(); colors = [].concat(colors.slice(0, hasSeries ? seriesLabels.length : labels.length)).reverse(); const ts = []; texts.forEach(text => { ts.push([].concat(text).reverse()); }); texts = ts; const ds = []; datasets.forEach(dataset => { ds.push([].concat(dataset).reverse()); }); datasets = ds; } const traces = []; const traceConfig = { type: model.chartType === "line" ? "line" : "bar", x: labels, customdata: hasSeries ? seriesLabels : labels, hoverinfo: hasSeries ? undefined : "x+y", orientation: "v", textposition: "none", }; if (model.type === "histogram" || !hasSeries) { traceConfig.width = 0.5; traceConfig.bargap = 0.5; traceConfig.mode = "markers", traceConfig.marker = { color: colors }; } datasets.forEach((dataset, index) => { var trace = Object.assign({}, traceConfig, { y: dataset, name: hasSeries ? seriesLabels[index] : labels[index], text: texts[index], }); if (model.showPercentages) { let texttemplate = model.showOnlyPercentages ? "%{text}%" : "%{value} (%{text}%)"; trace.textposition = "inside"; trace.texttemplate = texttemplate; if (!hasSeries) { trace.width = 0.9; trace.bargap = 0.1; } } traces.push(trace); }); const layout = { font: { family: "Segoe UI, sans-serif", size: 14, weight: "normal", color: "#404040", }, margin: { t: topMargin, b: bottomMargin, r: 10, }, colorway: colors, hovermode: "closest", plot_bgcolor: model.backgroundColor, paper_bgcolor: model.backgroundColor, showlegend: hasSeries, yaxis: { rangemode: "nonnegative", automargin: true, }, xaxis: { automargin: true, type: "category", tickmode: "array", tickvals: labels, ticktext: labels.map((label) => { return PlotlySetup.getTruncatedLabel(label, model.labelTruncateLength) + " "; }), }, }; if (model.showPercentages && model.showOnlyPercentages) { layout.yaxis = { automargin: true, tickformat: ".0%", range: [0, 1], ticklen: model.showOnlyPercentages ? 25 : 5, tickcolor: "transparent", }; } if (!model.getValueType || model.getValueType() != "date") { layout.xaxis = { automargin: true, type: "category", }; } return { traces, layout, hasSeries }; } static setupScatter(model, answersData) { let lineHeight = 30; let topMargin = 30; let bottomMargin = 30; let { datasets, labels, colors, texts, seriesLabels, } = answersData; const hasSeries = seriesLabels.length > 1 || model.question.getType() === "matrix"; const traces = []; const traceConfig = { type: "scatter", y: (hasSeries ? seriesLabels : labels).map((label) => { return PlotlySetup.getTruncatedLabel(label, model.labelTruncateLength); }), customdata: hasSeries ? seriesLabels : labels, text: hasSeries ? seriesLabels : labels, hoverinfo: "x+y", orientation: "h", mode: "markers", width: 0.5, marker: {}, }; if (!hasSeries) { traceConfig.marker.symbol = "circle"; traceConfig.marker.size = 16; } datasets.forEach((dataset) => { { var trace = Object.assign({}, traceConfig, { x: dataset, }); traces.push(trace); } }); const height = (labels.length + 1) * lineHeight + topMargin + bottomMargin; const layout = { font: { family: "Segoe UI, sans-serif", size: 14, weight: "normal", color: "#404040", }, height: height, margin: { t: topMargin, b: bottomMargin, r: 10, }, colorway: colors, hovermode: "closest", yaxis: { automargin: true, type: "category", ticklen: 5, tickcolor: "transparent", }, xaxis: { rangemode: "nonnegative", automargin: true, }, plot_bgcolor: model.backgroundColor, paper_bgcolor: model.backgroundColor, showlegend: false, }; if (hasSeries) { layout.showlegend = true; layout.height = undefined; labels.forEach((label, index) => { traces[index].hoverinfo = "x+name"; traces[index].marker.color = undefined; traces[index].name = label; }); } return { traces, layout, hasSeries }; } static setupGauge(model, answersData) { let [level, minValue, maxValue] = answersData; if (model.question.getType() === "rating") { const rateValues = model.question.visibleRateValues; maxValue = rateValues[rateValues.length - 1].value; minValue = rateValues[0].value; } const colors = model.generateColors(maxValue, minValue, NumberModel.stepsCount); if (NumberModel.showAsPercentage) { level = DataHelper.toPercentage(level, maxValue); minValue = DataHelper.toPercentage(minValue, maxValue); maxValue = DataHelper.toPercentage(maxValue, maxValue); } var traces = [ { type: "indicator", mode: "gauge+number", gauge: { axis: { range: [minValue, maxValue] }, shape: model.chartType, bgcolor: "white", bar: { color: colors[0] }, }, value: level, text: model.name, domain: { x: [0, 1], y: [0, 1] }, }, ]; const chartMargin = model.chartType === "bullet" ? 60 : 30; var layout = { height: 250, margin: { l: chartMargin, r: chartMargin, b: chartMargin, t: chartMargin, pad: 5 }, plot_bgcolor: model.backgroundColor, paper_bgcolor: model.backgroundColor, }; return { traces, layout, hasSeries: false }; } static setupRadar(model, answersData) { let { datasets, labels, colors, texts, seriesLabels, } = answersData; const hasSeries = seriesLabels.length > 1 || model.question.getType() === "matrix"; const traces = []; const traceConfig = { type: "scatterpolar", mode: "lines+markers", fill: "toself", line: { width: 2 }, marker: { size: 6 } }; datasets.forEach((dataset, index) => { const traceName = hasSeries ? seriesLabels[index] : ""; const trace = Object.assign({}, traceConfig, { r: dataset, theta: labels, name: traceName, text: texts[index], hoverinfo: "r+theta+name", customdata: labels, hovertemplate: "%{theta}: %{r}" + "<extra></extra>", line: Object.assign(Object.assign({}, traceConfig.line), { color: colors[index % colors.length] }), marker: Object.assign(Object.assign({}, traceConfig.marker), { color: colors[index % colors.length] }) }); traces.push(trace); }); const layout = { font: { family: "Segoe UI, sans-serif", size: 14, weight: "normal", color: "#404040", }, polar: { radialaxis: { visible: true, range: [0, Math.max(...datasets.map(s => Math.max(...s))) * 1.1], tickfont: { size: 12 } }, angularaxis: { tickfont: { size: 12 }, ticktext: labels.map((label) => { return PlotlySetup.getTruncatedLabel(label, model.labelTruncateLength); }), tickvals: labels } }, showlegend: hasSeries, colorway: colors, plot_bgcolor: model.backgroundColor, paper_bgcolor: model.backgroundColor, margin: { l: 50, r: 50, t: 50, b: 50 } }; return { traces, layout, hasSeries }; } } PlotlySetup.imageExportFormat = "png"; /** * Fires when end user clicks on the 'save as image' button. */ PlotlySetup.onImageSaving = new Event(); /** * Fires before plot will be created. User can change traces, layout and config of the plot. * Options is an object with the following fields: traces, layout and config of the plot. */ PlotlySetup.onPlotCreating = new Event(); PlotlySetup.setups = { bar: PlotlySetup.setupBar, vbar: PlotlySetup.setupVBar, line: PlotlySetup.setupVBar, stackedbar: PlotlySetup.setupBar, doughnut: PlotlySetup.setupPie, pie: PlotlySetup.setupPie, scatter: PlotlySetup.setupScatter, gauge: PlotlySetup.setupGauge, bullet: PlotlySetup.setupGauge, radar: PlotlySetup.setupRadar, }; PlotlySetup.getTruncatedLabel = (label, labelTruncateLength) => { const truncateSymbols = "..."; const truncateSymbolsLength = truncateSymbols.length; if (!labelTruncateLength) return label; if (labelTruncateLength === -1) return label; if (label.length <= labelTruncateLength + truncateSymbolsLength) return label; return label.substring(0, labelTruncateLength) + truncateSymbols; }; class MatrixDropdownGrouped extends SelectBase { constructor(question, data, options, name) { super(question, data, options, name || "matrixDropdownGrouped"); } get matrixQuestion() { return this.question; } get dataNames() { return this.matrixQuestion.columns.map(column => column.name); } getSeriesValues() { return this.matrixQuestion.columns.map((column) => column.name); } getSeriesLabels() { return this.matrixQuestion.columns.map((column) => column.title); } // public getSelectedItemByText(itemText: string) { // return this.matrixQuestion.columns.filter( // (column: ItemValue) => column.text === itemText // )[0]; // } valuesSource() { return this.matrixQuestion.choices; } isSupportMissingAnswers() { return false; } getCalculatedValuesCore() { const values = this.getValues(); const series = this.getSeriesValues(); const rows = this.matrixQuestion.rows.map(row => row.value); const statistics = defaultStatisticsCalculator(this.surveyData, { name: this.name, dataNames: series, getValues: () => values, getLabels: () => values, getSeriesValues: () => rows, getSeriesLabels: () => rows, }); return statistics.map(s => s[0]); } } VisualizationManager.registerVisualizer("matrixdropdown-grouped", MatrixDropdownGrouped); class SelectBasePlotly extends SelectBase { } SelectBasePlotly.types = ["bar", "vbar", "pie", "doughnut"]; SelectBasePlotly.displayModeBar = undefined; class BooleanPlotly extends BooleanModel { } BooleanPlotly.types = ["pie", "bar", "doughnut"]; class HistogramPlotly extends HistogramModel { } HistogramPlotly.types = ["vbar", "bar"]; class MatrixPlotly extends Matrix { } MatrixPlotly.types = ["bar", "stackedbar", "pie", "doughnut"]; class MatrixDropdownGroupedPlotly extends MatrixDropdownGrouped { } MatrixDropdownGroupedPlotly.types = ["stackedbar", "bar", "pie", "doughnut"]; class PivotPlotly extends PivotModel { } PivotPlotly.types = ["vbar", "bar", "line", "stackedbar", "pie", "doughnut"]; // ["vbar", "bar"]; class GaugePlotly extends NumberModel { } GaugePlotly.displayModeBar = undefined; GaugePlotly.types = ["gauge", "bullet"]; class RankingPlotly extends RankingModel { } const plotlyChartTypes = { "boolean": BooleanPlotly.types, "number": GaugePlotly.types, "selectBase": SelectBasePlotly.types, "histogram": HistogramPlotly.types, "matrix": MatrixPlotly.types, "matrixDropdownGrouped": MatrixDropdownGroupedPlotly.types, "pivot": PivotPlotly.types, "ranking": [].concat(SelectBasePlotly.types).concat(["radar"]), }; class PlotlyChartAdapter { constructor(model) { this.model = model; this._chart = undefined; } patchConfigParameters(chartNode, traces, layout, config) { if (this.model.question.getType() === "boolean") { const colors = this.model.getColors(); const boolColors = [ BooleanModel.trueColor || colors[0], BooleanModel.falseColor || colors[1], ]; if (this.model.showMissingAnswers) { boolColors.push(colors[2]); } const chartType = this.model.chartType; if (chartType === "pie" || chartType === "doughnut") { traces.forEach((trace) => { if (!trace) return; if (!trace.marker) trace.marker = {}; trace.marker.colors = boolColors; }); } else if (chartType === "bar") { traces.forEach((trace) => { if (!trace) return; if (!trace.marker) trace.marker = {}; trace.marker.color = boolColors; }); } } if (this.model.type === "number") { config.displayModeBar = true; } } get chart() { return this._chart; } getChartTypes() { const visualizerType = this.model.type; const chartCtypes = plotlyChartTypes[visualizerType]; return chartCtypes || []; } create(chartNode) { return __awaiter(this, void 0, void 0, function* () { const [plot, plotlyOptions] = yield this.update(chartNode); if (this.model instanceof SelectBase && this.model.supportSelection) { const _model = this.model; chartNode["on"]("plotly_click", (data) => { if (data.points.length > 0) { let itemText = ""; if (!plotlyOptions.hasSeries) { itemText = Array.isArray(data.points[0].customdata) ? data.points[0].customdata[0] : data.points[0].customdata; const item = _model.getSelectedItemByText(itemText); _model.setSelection(item); } else { itemText = data.points[0].data.name; const propertyLabel = data.points[0].label; const seriesValues = this.model.getSeriesValues(); const seriesLabels = this.model.getSeriesLabels(); const propertyValue = seriesValues[seriesLabels.indexOf(propertyLabel)]; const selectedItem = _model.getSelectedItemByText(itemText); const item = new ItemValue({ [propertyValue]: selectedItem.value }, propertyLabel + ": " + selectedItem.text); _model.setSelection(item); } // const itemText = plotlyOptions.hasSeries // ? data.points[0].data.name // : Array.isArray(data.points[0].customdata) // ? data.points[0].customdata[0] // : data.points[0].customdata; // const item: ItemValue = this.model.getSelectedItemByText(itemText); // this.model.setSelection(item); } }); } var getDragLayer = () => chartNode.getElementsByClassName("nsewdrag")[0]; chartNode["on"]("plotly_hover", () => { const dragLayer = getDragLayer(); dragLayer && (dragLayer.style.cursor = "pointer"); }); chartNode["on"]("plotly_unhover", () => { const dragLayer = getDragLayer(); dragLayer && (dragLayer.style.cursor = ""); }); // setTimeout(() => Plotly.Plots.resize(chartNode), 10); this._chart = plot; return plot; }); } update(chartNode) { return __awaiter(this, void 0, void 0, function* () { const answersData = (this.model instanceof SelectBase) ? yield this.model.getAnswersData() : yield this.model.getCalculatedValues(); var plotlyOptions = PlotlySetup.setup(this.model.chartType, this.model, answersData); let config = { displaylogo: false, responsive: true, locale: localization.currentLocale, modeBarButtonsToRemove: ["toImage"], modeBarButtonsToAdd: [ { name: "toImageSjs", title: localization.getString("saveDiagramAsPNG"), icon: Plotly.Icons.camera, click: (gd) => { let options = { format: PlotlySetup.imageExportFormat, // width: 800, // height: 600, filename: this.model.question.name, }; PlotlySetup.onImageSaving.fire(this.model, options); Plotly.downloadImage(gd, options); }, }, ], }; if (SelectBasePlotly.displayModeBar !== undefined) { config.displayModeBar = SelectBasePlotly.displayModeBar; } this.patchConfigParameters(chartNode, plotlyOptions.traces, plotlyOptions.layout, config); let options = { traces: plotlyOptions.traces, layout: plotlyOptions.layout, data: answersData, config: config, }; PlotlySetup.onPlotCreating.fire(this.model, options); const plot = Plotly.react(chartNode, options.traces, options.layout, options.config); return [plot, plotlyOptions]; }); } destroy(node) { if (!!node) { Plotly.purge(node); } this._chart = undefined; } } VisualizerBase.chartAdapterType = PlotlyChartAdapter; export { BooleanModel, BooleanPlotly, GaugePlotly, HistogramModel, HistogramPlotly, Matrix, MatrixDropdownGroupedPlotly, MatrixPlotly, NumberModel, PivotModel, PivotPlotly, PlotlyChartAdapter, PlotlySetup, RankingModel, RankingPlotly, SelectBase, SelectBasePlotly, VisualizationManager, VisualizerBase, localization, plotlyChartTypes }; //# sourceMappingURL=survey.analytics.mjs.map