survey-analytics
Version:
SurveyJS analytics Library.
740 lines (733 loc) • 29.5 kB
JavaScript
/*!
* 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