diff --git a/client/charts/SDChart/Axis.js b/client/charts/SDChart/Axis.js
index b020b650b..ba2e6175a 100644
--- a/client/charts/SDChart/Axis.js
+++ b/client/charts/SDChart/Axis.js
@@ -1,5 +1,5 @@
import {Series} from './Series';
-import {map, get} from 'lodash';
+import {map, get, sortBy, filter} from 'lodash';
/**
@@ -150,6 +150,25 @@ export class Axis {
* @description The maximum value on the x axis
*/
this.xMax = undefined;
+
+ /**
+ * @ngdoc property
+ * @name SDChart.Axis#sortOrder
+ * @type {String}
+ * @description If undefined, do not sort, otherwise sort categories in ascending or descending
+ * order based on series values (if series data is provided as an object)
+ */
+ this.sortOrder = undefined;
+
+ /**
+ * @ngdoc property
+ * @name SDChart.Axis#excludeEmpty
+ * @type {Boolean}
+ * @description If true, remove values with 0 from the categories and series
+ */
+ this.excludeEmpty = false;
+
+ this._sortedCategories = undefined;
}
/**
@@ -168,6 +187,9 @@ export class Axis {
* @param {string} [options.xTitle] - The title used on the x-axis
* @param {Number} [options.xMin] - The minimum value on the x axis
* @param {Number} [options.xMax] - The maximum value on the x axis
+ * @param {String} [options.sortOrder] - If undefined, do not sort, otherwise sort categories in
+ * ascending or descending order based on series values (if series data is provided as an object)
+ * @param {Boolean} [options.excludeEmpty=false] If true, remove values with 0 from the categories and series
* @return {SDChart.Axis}
* @description Sets the options for the axis
*/
@@ -199,16 +221,58 @@ export class Axis {
* @description Returns translated category field names
*/
getCategories() {
- if (this.categories !== undefined && this.categoryField !== undefined) {
- const names = this.chart.getTranslationNames(this.categoryField);
+ if (this._sortedCategories !== undefined) {
+ return this._sortedCategories;
+ }
- return map(
- this.categories,
- (category) => get(names, category) || category
- );
+ if (this.categories === undefined) {
+ return undefined;
}
- return this.categories;
+ let categories = this.categories;
+
+ if (this.series.length === 1 && typeof this.series[0].data === 'object') {
+ const data = this.series[0].data;
+
+ if (this.excludeEmpty === true) {
+ categories = filter(
+ categories,
+ (categoryId) => data[categoryId]
+ );
+ }
+
+ if (this.sortOrder !== undefined) {
+ categories = sortBy(
+ categories,
+ (categoryId) => (
+ this.sortOrder === 'asc' ? data[categoryId] : -data[categoryId]
+ )
+ );
+ }
+ }
+
+ this._sortedCategories = categories;
+ return categories;
+ }
+
+ /**
+ * @ngdoc method
+ * @name SDChart.Axis#getTranslatedCategories
+ * @return {Array}
+ * @description Returns categories sorted and translated
+ */
+ getTranslatedCategories() {
+ let categories = this.getCategories();
+
+ if (!this.categoryField) {
+ return categories;
+ }
+
+ const names = this.chart.getTranslationNames(this.categoryField);
+
+ return categories.map(
+ (categoryId) => names[categoryId] || categoryId
+ );
}
/**
@@ -223,6 +287,15 @@ export class Axis {
if (this.categories !== undefined) {
axisConfig.categories = this.getCategories();
+
+ if (this.categoryField !== undefined) {
+ const names = this.chart.getTranslationNames(this.categoryField);
+
+ axisConfig.categories = map(
+ axisConfig.categories,
+ (categoryId) => get(names, categoryId) || categoryId
+ );
+ }
}
if (this.allowDecimals !== undefined) {
diff --git a/client/charts/SDChart/Chart.js b/client/charts/SDChart/Chart.js
index e46f52b28..fc374752f 100644
--- a/client/charts/SDChart/Chart.js
+++ b/client/charts/SDChart/Chart.js
@@ -33,8 +33,12 @@ export class Chart {
* @param {Function} [options.tooltipFormatter] - Callback function to dynamically generate tooltips
* @param {Function} [options.onPointClick] - Callback function when a point is clicked
* @param {Object} [options.dataLabelConfig] - Custom config for dataLabels
- * @params {Boolean} [options.invertAxes=false] - Invert the X and Y axes
- * @params {Function} [options.legendFormatter] - Callback function to dynamically generate legend labels
+ * @param {Boolean} [options.invertAxes=false] - Invert the X and Y axes
+ * @param {Function} [options.legendFormatter] - Callback function to dynamically generate legend labels
+ * @param {String} [options.legendFormat] - The legend point format
+ * @param {Boolean} [options.shadow=true] - Creates a shadow around the chart container
+ * @param {Boolean} [options.exporting] - If false, then disables exporting options
+ * @param {Array} [options.legendOffset] - X/Y Offset for the legend position
* @description Initialise the data for the chart config
*/
constructor(options) {
@@ -66,6 +70,10 @@ export class Chart {
options.invertAxes = false;
}
+ if (!('shadow' in options)) {
+ options.shadow = true;
+ }
+
/**
* @ngdoc property
* @name SDChart.Chart#id
@@ -271,6 +279,38 @@ export class Chart {
* @description Callback function to dynamically generate legend labels
*/
this.legendFormatter = options.legendFormatter;
+
+ /**
+ * @ngdoc property
+ * @name SDChart.Chart#legendFormat
+ * @type {String}
+ * @description The legend point format
+ */
+ this.legendFormat = options.legendFormat;
+
+ /**
+ * @ngdoc property
+ * @name SDChart.Chart#shadow
+ * @type {Boolean}
+ * @description Creates a shadow around the chart container
+ */
+ this.shadow = options.shadow;
+
+ /**
+ * @ngdoc property
+ * @name SDChart.Chart#exporting
+ * @type {Boolean}
+ * @description If false, then disables exporting options
+ */
+ this.exporting = options.exporting;
+
+ /**
+ * @ngdoc property
+ * @name SDChart.Chart#legendOffset
+ * @ype {Array}
+ * @description X/Y Offset for the legend position
+ */
+ this.legendOffset = options.legendOffset;
}
/**
@@ -292,11 +332,14 @@ export class Chart {
genTitleConfig(config) {
const title = this.getTitle();
+ if (!get(config, 'title')) {
+ config.title = {};
+ }
+
if (title !== undefined) {
- if (!get(config, 'title')) {
- config.title = {};
- }
config.title.text = title;
+ } else if (!get(config, 'title.text')) {
+ config.title.text = '';
}
return config;
@@ -342,16 +385,28 @@ export class Chart {
config.legend = {};
}
- if (this.legendTitle === undefined) {
+ if (!this.legendTitle && !this.legendFormatter && !this.legendFormat) {
config.legend.enabled = false;
} else {
config.legend.enabled = true;
- config.legend.title = {text: this.legendTitle};
- }
-
- if (this.legendFormatter !== undefined) {
- config.legend.labelFormatter = this.legendFormatter;
config.legend.useHTML = true;
+
+ if (this.legendTitle) {
+ config.legend.title = {text: this.legendTitle};
+ }
+
+ if (this.legendFormatter) {
+ config.legend.labelFormatter = this.legendFormatter;
+ }
+
+ if (this.legendFormat) {
+ config.legend.labelFormat = this.legendFormat;
+ }
+
+ if (this.legendOffset) {
+ config.legend.x = this.legendOffset[0];
+ config.legend.y = this.legendOffset[1];
+ }
}
return config;
@@ -368,17 +423,22 @@ export class Chart {
config.tooltip = {};
}
- if (this.tooltipHeader !== undefined) {
- config.tooltip.headerFormat = this.tooltipHeader;
- }
+ if (!this.tooltipHeader && !this.tooltipPoint && !this.tooltipFormatter) {
+ config.tooltip.enabled = false;
+ } else {
+ config.tooltip.enabled = true;
+ if (this.tooltipHeader !== undefined) {
+ config.tooltip.headerFormat = this.tooltipHeader;
+ }
- if (this.tooltipPoint !== undefined) {
- config.tooltip.pointFormat = this.tooltipPoint;
- }
+ if (this.tooltipPoint !== undefined) {
+ config.tooltip.pointFormat = this.tooltipPoint;
+ }
- if (this.tooltipFormatter !== undefined) {
- config.tooltip.formatter = this.tooltipFormatter;
- config.tooltip.useHTML = true;
+ if (this.tooltipFormatter !== undefined) {
+ config.tooltip.formatter = this.tooltipFormatter;
+ config.tooltip.useHTML = true;
+ }
}
return config;
@@ -529,6 +589,18 @@ export class Chart {
return config;
}
+ /**
+ * @ngdoc method
+ * @name SDChart.Chart#genExportingConfig
+ * @param {Object} config
+ * @description Generates the exporting config to use for the chart
+ */
+ genExportingConfig(config) {
+ if (this.exporting === false) {
+ config.exporting = {enabled: false};
+ }
+ }
+
/**
* @ngdoc method
* @name SDChart.Chart#genHighchartsConfig
@@ -538,6 +610,7 @@ export class Chart {
genHighchartsConfig(config) {
config.id = this.id;
config.type = this.chartType;
+ config.shadow = this.shadow;
this.genChartConfig(config);
this.genTitleConfig(config);
@@ -546,6 +619,7 @@ export class Chart {
this.genLegendConfig(config);
this.genTooltipConfig(config);
this.genPlotConfig(config);
+ this.genExportingConfig(config);
this.axis.forEach((axis) => axis.genConfig(config));
@@ -563,7 +637,7 @@ export class Chart {
const headers = [axis.xTitle, axis.yTitle];
const rows = [];
- forEach(axis.getCategories(), (category, index) => {
+ forEach(axis.getTranslatedCategories(), (category, index) => {
rows.push([
category,
axis.series[0].data[index],
@@ -597,7 +671,7 @@ export class Chart {
'Total Stories'
);
- const rows = axis.getCategories().map((category) => ([category]));
+ const rows = axis.getTranslatedCategories().map((category) => ([category]));
forEach(axis.series, (series) => {
series.getData().forEach((count, index) => {
diff --git a/client/charts/SDChart/Series.js b/client/charts/SDChart/Series.js
index 42511e9a5..1b3bcefd5 100644
--- a/client/charts/SDChart/Series.js
+++ b/client/charts/SDChart/Series.js
@@ -92,18 +92,63 @@ export class Series {
* @description Config to use for the data labels of this series
*/
this.dataLabelConfig = undefined;
+
+ /**
+ * @ngdoc property
+ * @name SDChart.Series#colours
+ * @type {Array|Object}
+ * @description Array or Object of colours to use for the data points
+ */
+ this.colours = undefined;
+
+ /**
+ * @ngdoc property
+ * @name SDChart.Series#size
+ * @type {Number}
+ * @description Size of the chart
+ */
+ this.size = undefined;
+
+ /**
+ * @ngdoc property
+ * @name SDChart.Series#semiCircle
+ * @type {Boolean}
+ * @description If the chart type is a pie, then render a semi circle
+ */
+ this.semiCircle = undefined;
+
+ /**
+ * @ngdoc property
+ * @name SDChart.Series#center
+ * @type {Array}
+ * @description The x and y offset of the center of the chart
+ */
+ this.center = undefined;
+
+ /**
+ * @ngdoc property
+ * @name SDChart.Series#showInLegend
+ * @type {Boolean}
+ * @description If true, show the point names in the legend
+ */
+ this.showInLegend = undefined;
}
/**
* @ngdoc method
* @name SDChart.Series#setOptions
* @param {string} [options.type=this.axis.defaultChartType] - The chart type
- * @param {Object|Array} options.data - The data to add to the series
+ * @param {Object|Array} [options.data] - The data to add to the series
* @param {string} [options.field] - The field type for the data
* @param {string} [options.name] - The field name for the data
* @param {Number} [options.stack] - The stack number
* @param {string} [options.stackType] - The type of stacking to perform
* @param {string} [options.colourIndex] - The colour index to use for this series
+ * @param {Array|Object} [options.colours] - Array or Object of colours to use for the data points
+ * @param {Number} [options.size] - Size of the chart
+ * @param {Boolean} [options.semiCircle] - If the chart type is a pie, then render a semi circle
+ * @param {Array} [options.center] - The x and y offset of the center of the chart
+ * @param {Boolean} [options.showInLegend] - If true, show the point names in the legend
* @return {SDChart.Series}
* @description Sets the options for the series
*/
@@ -121,17 +166,18 @@ export class Series {
* @return {Array}
* @description Returns the data for this series
*/
- getData() {
- if (this.data === undefined) {
- return undefined;
- } else if (Array.isArray(this.data)) {
+ getData(series) {
+ if (this.data === undefined || Array.isArray(this.data)) {
return this.data;
- }
+ } else if (this.axis.categories !== undefined) {
+ const names = this.chart.getTranslationNames(this.axis.categoryField);
- if (this.axis.categories !== undefined) {
- return map(
- this.axis.categories,
- (source) => get(this.data, source) || 0
+ return this.axis.getCategories().map(
+ (categoryId, index) => ({
+ name: names[categoryId] || categoryId,
+ y: get(this.data, categoryId) || 0,
+ className: get(this.colours, categoryId) || get(this.colours, index) || '',
+ })
);
}
@@ -167,7 +213,7 @@ export class Series {
*/
setDataConfig(series) {
const name = this.getName();
- const data = this.getData();
+ const data = this.getData(series);
if (name !== undefined) {
series.name = name;
@@ -197,6 +243,25 @@ export class Series {
if (this.axis.pointInterval !== undefined) {
series.pointInterval = this.axis.pointInterval;
}
+
+ if (this.size) {
+ series.size = this.size;
+ }
+
+ if (series.type === 'pie' && this.semiCircle !== undefined) {
+ series.startAngle = -90;
+ series.endAngle = 90;
+ series.innerSize = '50%';
+ series.slicedOffset = 0;
+ }
+
+ if (this.center !== undefined) {
+ series.center = this.center;
+ }
+
+ if (this.showInLegend !== undefined) {
+ series.showInLegend = this.showInLegend;
+ }
}
/**
@@ -232,6 +297,7 @@ export class Series {
this.setPointConfig(series);
this.setStyleConfig(series);
+
return series;
}
}
diff --git a/client/charts/SDChart/tests/SDChart.Axis.spec.js b/client/charts/SDChart/tests/SDChart.Axis.spec.js
index b18fdf7a7..fcc50dec7 100644
--- a/client/charts/SDChart/tests/SDChart.Axis.spec.js
+++ b/client/charts/SDChart/tests/SDChart.Axis.spec.js
@@ -161,4 +161,122 @@ describe('SDChart.Axis', () => {
}],
}));
});
+
+ it('can sort categories', () => {
+ let chart = new SDChart.Chart({});
+
+ chart.addAxis()
+ .setOptions({
+ type: 'category',
+ categories: ['a', 'b', 'c'],
+ sortOrder: 'asc',
+ xTitle: 'Category',
+ })
+ .addSeries()
+ .setOptions({
+ data: {a: 1, b: 10, c: 3},
+ });
+
+ expect(chart.genConfig()).toEqual(jasmine.objectContaining({
+ xAxis: [{
+ type: 'category',
+ allowDecimals: false,
+ categories: ['a', 'c', 'b'],
+ title: {text: 'Category'},
+ }],
+ }));
+
+ chart = new SDChart.Chart({});
+
+ chart.addAxis()
+ .setOptions({
+ type: 'category',
+ categories: ['a', 'b', 'c'],
+ sortOrder: 'desc',
+ xTitle: 'Category',
+ })
+ .addSeries()
+ .setOptions({
+ data: {a: 1, b: 10, c: 3},
+ });
+
+ expect(chart.genConfig()).toEqual(jasmine.objectContaining({
+ xAxis: [{
+ type: 'category',
+ allowDecimals: false,
+ categories: ['b', 'c', 'a'],
+ title: {text: 'Category'},
+ }],
+ }));
+
+ chart = new SDChart.Chart({});
+
+ chart.addAxis()
+ .setOptions({
+ type: 'category',
+ categories: ['a', 'b', 'c'],
+ xTitle: 'Category',
+ })
+ .addSeries()
+ .setOptions({
+ data: {a: 1, b: 10, c: 3},
+ });
+
+ expect(chart.genConfig()).toEqual(jasmine.objectContaining({
+ xAxis: [{
+ type: 'category',
+ allowDecimals: false,
+ categories: ['a', 'b', 'c'],
+ title: {text: 'Category'},
+ }],
+ }));
+ });
+
+ it('can exclude empty data', () => {
+ let chart = new SDChart.Chart({});
+
+ chart.addAxis()
+ .setOptions({
+ type: 'category',
+ categories: ['a', 'b', 'c'],
+ xTitle: 'Category',
+ excludeEmpty: true,
+ })
+ .addSeries()
+ .setOptions({
+ data: {a: 1, b: 0, c: 3},
+ });
+
+ expect(chart.genConfig()).toEqual(jasmine.objectContaining({
+ xAxis: [{
+ type: 'category',
+ allowDecimals: false,
+ categories: ['a', 'c'],
+ title: {text: 'Category'},
+ }],
+ }));
+
+ chart = new SDChart.Chart({});
+
+ chart.addAxis()
+ .setOptions({
+ type: 'category',
+ categories: ['a', 'b', 'c'],
+ xTitle: 'Category',
+ excludeEmpty: false,
+ })
+ .addSeries()
+ .setOptions({
+ data: {a: 1, b: 0, c: 3},
+ });
+
+ expect(chart.genConfig()).toEqual(jasmine.objectContaining({
+ xAxis: [{
+ type: 'category',
+ allowDecimals: false,
+ categories: ['a', 'b', 'c'],
+ title: {text: 'Category'},
+ }],
+ }));
+ });
});
diff --git a/client/charts/SDChart/tests/SDChart.Chart.spec.js b/client/charts/SDChart/tests/SDChart.Chart.spec.js
index 71ebfc76e..b59d6fbee 100644
--- a/client/charts/SDChart/tests/SDChart.Chart.spec.js
+++ b/client/charts/SDChart/tests/SDChart.Chart.spec.js
@@ -14,9 +14,11 @@ describe('SDChart.Chart', () => {
chart: {},
time: {useUTC: true},
legend: {enabled: false},
- tooltip: {},
+ tooltip: {enabled: false},
plotOptions: {series: {dataLabels: {enabled: false}}},
fullHeight: false,
+ shadow: true,
+ title: {text: ''},
});
expect(genConfig({
@@ -33,7 +35,7 @@ describe('SDChart.Chart', () => {
chart: {},
time: {useUTC: true},
legend: {enabled: false},
- tooltip: {},
+ tooltip: {enabled: false},
plotOptions: {
series: {
dataLabels: {enabled: false},
@@ -44,6 +46,7 @@ describe('SDChart.Chart', () => {
title: {text: 'Default Title'},
subtitle: {text: 'Default Subtitle'},
fullHeight: false,
+ shadow: true,
});
});
@@ -93,6 +96,7 @@ describe('SDChart.Chart', () => {
legend: {
enabled: true,
title: {text: 'Test Legend'},
+ useHTML: true,
},
}));
@@ -103,6 +107,7 @@ describe('SDChart.Chart', () => {
})
).toEqual(jasmine.objectContaining({
tooltip: {
+ enabled: true,
headerFormat: 'Tool Header {point.x}',
pointFormat: 'Tool Point {point.y}',
},
diff --git a/client/charts/SDChart/tests/SDChart.Series.spec.js b/client/charts/SDChart/tests/SDChart.Series.spec.js
index 7d9d3b6b3..a22a614af 100644
--- a/client/charts/SDChart/tests/SDChart.Series.spec.js
+++ b/client/charts/SDChart/tests/SDChart.Series.spec.js
@@ -147,7 +147,11 @@ describe('SDChart.Series', () => {
xAxis: 0,
type: 'bar',
name: 'Test Data',
- data: [6, 10, 2],
+ data: [
+ {name: 'b', y: 6, className: ''},
+ {name: 'c', y: 10, className: ''},
+ {name: 'a', y: 2, className: ''},
+ ],
}],
}));
@@ -184,7 +188,371 @@ describe('SDChart.Series', () => {
xAxis: 0,
type: 'bar',
name: 'Category',
- data: [6, 10, 2],
+ data: [
+ {name: 'Basketball', y: 6, className: ''},
+ {name: 'Cricket', y: 10, className: ''},
+ {name: 'Advisories', y: 2, className: ''},
+ ],
+ }],
+ }));
+ });
+
+ it('can sort data', () => {
+ let chart = new SDChart.Chart({});
+
+ chart.addAxis()
+ .setOptions({
+ type: 'category',
+ categories: ['a', 'b', 'c'],
+ sortOrder: 'asc',
+ xTitle: 'Category',
+ })
+ .addSeries()
+ .setOptions({
+ data: {a: 1, b: 10, c: 3},
+ });
+
+ expect(chart.genConfig()).toEqual(jasmine.objectContaining({
+ series: [{
+ xAxis: 0,
+ type: 'bar',
+ data: [
+ {name: 'a', y: 1, className: ''},
+ {name: 'c', y: 3, className: ''},
+ {name: 'b', y: 10, className: ''},
+ ],
+ }],
+ }));
+
+ chart = new SDChart.Chart({});
+
+ chart.addAxis()
+ .setOptions({
+ type: 'category',
+ categories: ['a', 'b', 'c'],
+ sortOrder: 'desc',
+ xTitle: 'Category',
+ })
+ .addSeries()
+ .setOptions({
+ data: {a: 1, b: 10, c: 3},
+ });
+
+ expect(chart.genConfig()).toEqual(jasmine.objectContaining({
+ series: [{
+ xAxis: 0,
+ type: 'bar',
+ data: [
+ {name: 'b', y: 10, className: ''},
+ {name: 'c', y: 3, className: ''},
+ {name: 'a', y: 1, className: ''},
+ ],
+ }],
+ }));
+
+ chart = new SDChart.Chart({});
+
+ chart.addAxis()
+ .setOptions({
+ type: 'category',
+ categories: ['a', 'b', 'c'],
+ xTitle: 'Category',
+ })
+ .addSeries()
+ .setOptions({
+ data: {a: 1, b: 10, c: 3},
+ });
+
+ expect(chart.genConfig()).toEqual(jasmine.objectContaining({
+ series: [{
+ xAxis: 0,
+ type: 'bar',
+ data: [
+ {name: 'a', y: 1, className: ''},
+ {name: 'b', y: 10, className: ''},
+ {name: 'c', y: 3, className: ''},
+ ],
+ }],
+ }));
+ });
+
+ it('can exclude empty data', () => {
+ let chart = new SDChart.Chart({});
+
+ chart.addAxis()
+ .setOptions({
+ type: 'category',
+ categories: ['a', 'b', 'c'],
+ xTitle: 'Category',
+ excludeEmpty: true,
+ })
+ .addSeries()
+ .setOptions({
+ data: {a: 1, b: 0, c: 3},
+ });
+
+ expect(chart.genConfig()).toEqual(jasmine.objectContaining({
+ series: [{
+ xAxis: 0,
+ type: 'bar',
+ data: [
+ {name: 'a', y: 1, className: ''},
+ {name: 'c', y: 3, className: ''},
+ ],
+ }],
+ }));
+
+ chart = new SDChart.Chart({});
+
+ chart.addAxis()
+ .setOptions({
+ type: 'category',
+ categories: ['a', 'b', 'c'],
+ xTitle: 'Category',
+ excludeEmpty: false,
+ })
+ .addSeries()
+ .setOptions({
+ data: {a: 1, b: 0, c: 3},
+ });
+
+ expect(chart.genConfig()).toEqual(jasmine.objectContaining({
+ series: [{
+ xAxis: 0,
+ type: 'bar',
+ data: [
+ {name: 'a', y: 1, className: ''},
+ {name: 'b', y: 0, className: ''},
+ {name: 'c', y: 3, className: ''},
+ ],
+ }],
+ }));
+ });
+
+ it('can set colours', () => {
+ let chart = new SDChart.Chart({});
+
+ chart.addAxis()
+ .setOptions({
+ type: 'category',
+ categories: ['a', 'b', 'c'],
+ xTitle: 'Category',
+ })
+ .addSeries()
+ .setOptions({
+ data: {a: 1, b: 10, c: 3},
+ colours: {a: 'sda-blue', b: 'sda-green', c: 'sda-orange'},
+ });
+
+ expect(chart.genConfig()).toEqual(jasmine.objectContaining({
+ series: [{
+ xAxis: 0,
+ type: 'bar',
+ data: [
+ {name: 'a', y: 1, className: 'sda-blue'},
+ {name: 'b', y: 10, className: 'sda-green'},
+ {name: 'c', y: 3, className: 'sda-orange'},
+ ],
+ }],
+ }));
+
+ chart = new SDChart.Chart({});
+
+ chart.addAxis()
+ .setOptions({
+ type: 'category',
+ categories: ['a', 'b', 'c'],
+ xTitle: 'Category',
+ })
+ .addSeries()
+ .setOptions({
+ data: {a: 1, b: 10, c: 3},
+ colours: ['sda-blue', 'sda-green', 'sda-orange'],
+ });
+
+ expect(chart.genConfig()).toEqual(jasmine.objectContaining({
+ series: [{
+ xAxis: 0,
+ type: 'bar',
+ data: [
+ {name: 'a', y: 1, className: 'sda-blue'},
+ {name: 'b', y: 10, className: 'sda-green'},
+ {name: 'c', y: 3, className: 'sda-orange'},
+ ],
+ }],
+ }));
+ });
+
+ it('can set the size', () => {
+ const chart = new SDChart.Chart({});
+
+ chart.addAxis()
+ .setOptions({
+ type: 'category',
+ categories: ['a', 'b', 'c'],
+ xTitle: 'Category',
+ })
+ .addSeries()
+ .setOptions({
+ data: {a: 1, b: 10, c: 3},
+ size: 260,
+ });
+
+ expect(chart.genConfig()).toEqual(jasmine.objectContaining({
+ series: [{
+ xAxis: 0,
+ type: 'bar',
+ size: 260,
+ data: [
+ {name: 'a', y: 1, className: ''},
+ {name: 'b', y: 10, className: ''},
+ {name: 'c', y: 3, className: ''},
+ ],
+ }],
+ }));
+ });
+
+ it('can set pie chart to be a semi-circle', () => {
+ let chart = new SDChart.Chart({});
+
+ chart.addAxis()
+ .setOptions({
+ type: 'category',
+ defaultChartType: 'pie',
+ categories: ['a', 'b', 'c'],
+ xTitle: 'Category',
+ })
+ .addSeries()
+ .setOptions({
+ data: {a: 1, b: 10, c: 3},
+ semiCircle: true,
+ });
+
+ expect(chart.genConfig()).toEqual(jasmine.objectContaining({
+ series: [{
+ xAxis: 0,
+ type: 'pie',
+ startAngle: -90,
+ endAngle: 90,
+ innerSize: '50%',
+ slicedOffset: 0,
+ data: [
+ {name: 'a', y: 1, className: ''},
+ {name: 'b', y: 10, className: ''},
+ {name: 'c', y: 3, className: ''},
+ ],
+ }],
+ }));
+
+ // Ignores semiCircle if chart type is not a pie chart
+ chart = new SDChart.Chart({});
+ chart.addAxis()
+ .setOptions({
+ type: 'category',
+ defaultChartType: 'bar',
+ categories: ['a', 'b', 'c'],
+ xTitle: 'Category',
+ })
+ .addSeries()
+ .setOptions({
+ data: {a: 1, b: 10, c: 3},
+ semiCircle: true,
+ });
+
+ expect(chart.genConfig()).toEqual(jasmine.objectContaining({
+ series: [{
+ xAxis: 0,
+ type: 'bar',
+ data: [
+ {name: 'a', y: 1, className: ''},
+ {name: 'b', y: 10, className: ''},
+ {name: 'c', y: 3, className: ''},
+ ],
+ }],
+ }));
+ });
+
+ it('can set the center of the chart', () => {
+ const chart = new SDChart.Chart({});
+
+ chart.addAxis()
+ .setOptions({
+ type: 'category',
+ categories: ['a', 'b', 'c'],
+ xTitle: 'Category',
+ })
+ .addSeries()
+ .setOptions({
+ data: {a: 1, b: 10, c: 3},
+ center: ['50%', '75%'],
+ });
+
+ expect(chart.genConfig()).toEqual(jasmine.objectContaining({
+ series: [{
+ xAxis: 0,
+ type: 'bar',
+ center: ['50%', '75%'],
+ data: [
+ {name: 'a', y: 1, className: ''},
+ {name: 'b', y: 10, className: ''},
+ {name: 'c', y: 3, className: ''},
+ ],
+ }],
+ }));
+ });
+
+ it('can set showInLegend of the chart', () => {
+ let chart = new SDChart.Chart({});
+
+ chart.addAxis()
+ .setOptions({
+ type: 'category',
+ categories: ['a', 'b', 'c'],
+ xTitle: 'Category',
+ })
+ .addSeries()
+ .setOptions({
+ data: {a: 1, b: 10, c: 3},
+ showInLegend: true,
+ });
+
+ expect(chart.genConfig()).toEqual(jasmine.objectContaining({
+ series: [{
+ xAxis: 0,
+ type: 'bar',
+ showInLegend: true,
+ data: [
+ {name: 'a', y: 1, className: ''},
+ {name: 'b', y: 10, className: ''},
+ {name: 'c', y: 3, className: ''},
+ ],
+ }],
+ }));
+
+ chart = new SDChart.Chart({});
+
+ chart.addAxis()
+ .setOptions({
+ type: 'category',
+ categories: ['a', 'b', 'c'],
+ xTitle: 'Category',
+ })
+ .addSeries()
+ .setOptions({
+ data: {a: 1, b: 10, c: 3},
+ showInLegend: false,
+ });
+
+ expect(chart.genConfig()).toEqual(jasmine.objectContaining({
+ series: [{
+ xAxis: 0,
+ type: 'bar',
+ showInLegend: false,
+ data: [
+ {name: 'a', y: 1, className: ''},
+ {name: 'b', y: 10, className: ''},
+ {name: 'c', y: 3, className: ''},
+ ],
}],
}));
});
diff --git a/client/charts/directives/Chart.js b/client/charts/directives/Chart.js
index e6dcc9bdd..3b80129b7 100644
--- a/client/charts/directives/Chart.js
+++ b/client/charts/directives/Chart.js
@@ -1,4 +1,4 @@
-Chart.$inject = ['chartManager', '$timeout', '$sce'];
+Chart.$inject = ['chartManager', '$timeout', '$sce', 'lodash'];
/**
* @ngdoc directive
@@ -8,19 +8,25 @@ Chart.$inject = ['chartManager', '$timeout', '$sce'];
* @requires $timeout
* @description A directive that renders a Highcharts instance given its config
*/
-export function Chart(chartManager, $timeout, $sce) {
+export function Chart(chartManager, $timeout, $sce, _) {
return {
scope: {config: '<'},
template: require('../views/chart.html'),
link: function(scope, element, attrs) {
let target = element.find('div');
- scope.$watch('config', (config) => {
- render(config, config.id);
+ scope.$watch('config', (newConfig, oldConfig) => {
+ if (newConfig) {
+ render(newConfig, newConfig.id);
+ } else if (_.get(oldConfig, 'id')) {
+ chartManager.destroy(oldConfig.id);
+ }
});
scope.$on('$destroy', () => {
- chartManager.destroy(scope.config.id);
+ if (_.get(scope, 'config.id')) {
+ chartManager.destroy(scope.config.id);
+ }
});
/**
diff --git a/client/charts/directives/ChartColourPicker.js b/client/charts/directives/ChartColourPicker.js
new file mode 100644
index 000000000..0bd0e3ab6
--- /dev/null
+++ b/client/charts/directives/ChartColourPicker.js
@@ -0,0 +1,73 @@
+ChartColourPicker.$inject = ['gettext'];
+
+
+export const CHART_COLOURS = {
+ BLACK: 'sda-black',
+ GRAY_DARKER: 'sda-gray-darker',
+ GRAY_DARK: 'sda-gray-dark',
+ GRAY_MEDIUM: 'sda-gray-medium',
+ GRAY: 'sda-gray',
+ GRAY_NEUTRAL: 'sda-gray-neutral',
+ GRAY_TEXT: 'sda-gray-text',
+ GRAY_LIGHT: 'sda-gray-light',
+ GRAY_LIGHTER: 'sda-gray-lighter',
+ WHITE: 'sda-white',
+ BLUE: 'sda-blue',
+ BLUE_MEDIUM: 'sda-blue-medium',
+ BLUE_DARK: 'sda-blue-dark',
+ GREEN: 'sda-green',
+ RED: 'sda-red',
+ YELLOW: 'sda-yellow',
+ ORANGE: 'sda-orange',
+ PURPLE: 'sda-purple',
+ FERN_GREEN: 'sda-fern-green',
+ OLD_GOLD: 'sda-old-gold',
+ DARK_ORNGE: 'sda-dark-orange',
+ FIRE_BRICK: 'sda-fire-brick',
+ DEEP_PINK: 'sda-deep-pink',
+ DARK_MAGENTA: 'sda-dark-magenta',
+ DARK_VIOLET: 'sda-dark-violet',
+ NAVY: 'sda-navy',
+};
+
+export function ChartColourPicker(gettext) {
+ return {
+ scope: {
+ field: '=',
+ label: '=',
+ },
+ replace: true,
+ transclude: true,
+ template: require('../views/chart-colour-picker.html'),
+ link: function(scope) {
+ scope.colours = [
+ {name: gettext('Black'), colour: CHART_COLOURS.BLACK},
+ {name: gettext('Dark Gray'), colour: CHART_COLOURS.GRAY_DARK},
+ {name: gettext('Darker Gray'), colour: CHART_COLOURS.GRAY_DARKER},
+ {name: gettext('Medium Gray'), colour: CHART_COLOURS.GRAY_MEDIUM},
+ {name: gettext('Gray'), colour: CHART_COLOURS.GRAY},
+ {name: gettext('Neutral Gray'), colour: CHART_COLOURS.GRAY_NEUTRAL},
+ {name: gettext('Gray Text'), colour: CHART_COLOURS.GRAY_TEXT},
+ {name: gettext('Light Gray'), colour: CHART_COLOURS.GRAY_LIGHT},
+ {name: gettext('Lighter Gray'), colour: CHART_COLOURS.GRAY_LIGHTER},
+ {name: gettext('White'), colour: CHART_COLOURS.WHITE},
+ {name: gettext('Blue'), colour: CHART_COLOURS.BLUE},
+ {name: gettext('Medium Blue'), colour: CHART_COLOURS.BLUE_MEDIUM},
+ {name: gettext('Dark Blue'), colour: CHART_COLOURS.BLUE_DARK},
+ {name: gettext('Green'), colour: CHART_COLOURS.GREEN},
+ {name: gettext('Red'), colour: CHART_COLOURS.RED},
+ {name: gettext('Yellow'), colour: CHART_COLOURS.YELLOW},
+ {name: gettext('Orange'), colour: CHART_COLOURS.ORANGE},
+ {name: gettext('Purple'), colour: CHART_COLOURS.PURPLE},
+ {name: gettext('Fern Green'), colour: CHART_COLOURS.FERN_GREEN},
+ {name: gettext('Old Gold'), colour: CHART_COLOURS.OLD_GOLD},
+ {name: gettext('Dark Orange'), colour: CHART_COLOURS.DARK_ORNGE},
+ {name: gettext('Fire Brick'), colour: CHART_COLOURS.FIRE_BRICK},
+ {name: gettext('Deep Pink'), colour: CHART_COLOURS.DEEP_PINK},
+ {name: gettext('Dark Magenta'), colour: CHART_COLOURS.DARK_MAGENTA},
+ {name: gettext('Dark Violet'), colour: CHART_COLOURS.DARK_VIOLET},
+ {name: gettext('Navy'), colour: CHART_COLOURS.NAVY},
+ ];
+ },
+ };
+}
diff --git a/client/charts/directives/ChartOptions.js b/client/charts/directives/ChartOptions.js
new file mode 100644
index 000000000..c40e8ef87
--- /dev/null
+++ b/client/charts/directives/ChartOptions.js
@@ -0,0 +1,66 @@
+ChartOptions.$inject = ['chartConfig'];
+
+export const CHART_FIELDS = {
+ TITLE: 'title',
+ SUBTITLE: 'subtitle',
+ TYPE: 'type',
+ SORT: 'sort',
+ PAGE_SIZE: 'page_size',
+};
+
+export const DEFAULT_CHART_FIELDS = [
+ CHART_FIELDS.TITLE,
+ CHART_FIELDS.SUBTITLE,
+ CHART_FIELDS.TYPE,
+ CHART_FIELDS.SORT,
+];
+
+export const CHART_TYPES = {
+ BAR: 'bar',
+ COLUMN: 'column',
+ TABLE: 'table',
+ AREA: 'area',
+ LINE: 'line',
+ PIE: 'pie',
+ SCATTER: 'scatter',
+ SPLINE: 'spline',
+};
+
+export const DEFAULT_CHART_TYPES = [
+ CHART_TYPES.BAR,
+ CHART_TYPES.COLUMN,
+ CHART_TYPES.TABLE,
+];
+
+export function ChartOptions(chartConfig) {
+ return {
+ scope: {
+ params: '=',
+ fields: '=?',
+ chartTypes: '=?',
+ titlePlaceholder: '=?',
+ subtitlePlaceholder: '=?',
+ updateChartConfig: '=?',
+ },
+ template: require('../views/chart-form-options.html'),
+ link: function(scope) {
+ if (angular.isUndefined(scope.fields)) {
+ scope.fields = DEFAULT_CHART_FIELDS;
+ }
+
+ scope.enabled = {};
+
+ Object.values(CHART_FIELDS).forEach(
+ (field) => {
+ scope.enabled[field] = scope.fields.indexOf(field) > -1;
+ }
+ );
+
+ if (angular.isUndefined(scope.chartTypes)) {
+ scope.chartTypes = DEFAULT_CHART_TYPES;
+ }
+
+ scope.types = chartConfig.filterChartTypes(scope.chartTypes);
+ },
+ };
+}
diff --git a/client/charts/directives/index.js b/client/charts/directives/index.js
index e6edef497..8cf177e1d 100644
--- a/client/charts/directives/index.js
+++ b/client/charts/directives/index.js
@@ -1,3 +1,5 @@
export {Chart} from './Chart';
export {ChartContainer} from './ChartContainer';
export {Table} from './Table';
+export {ChartOptions} from './ChartOptions';
+export {ChartColourPicker} from './ChartColourPicker';
diff --git a/client/charts/index.js b/client/charts/index.js
index 8db475021..3ce3f640c 100644
--- a/client/charts/index.js
+++ b/client/charts/index.js
@@ -46,5 +46,7 @@ angular.module('superdesk.analytics.charts', [])
.directive('sdaChart', directives.Chart)
.directive('sdaChartContainer', directives.ChartContainer)
.directive('sdaTable', directives.Table)
+ .directive('sdaChartOptions', directives.ChartOptions)
+ .directive('sdaChartColourPicker', directives.ChartColourPicker)
.run(cacheIncludedTemplates);
diff --git a/client/charts/services/ChartConfig.js b/client/charts/services/ChartConfig.js
index 1f7412eb9..2db69b549 100644
--- a/client/charts/services/ChartConfig.js
+++ b/client/charts/services/ChartConfig.js
@@ -143,6 +143,7 @@ export function ChartConfig(
this.chartType = chartType;
this.sources = [];
this.sortOrder = 'desc';
+ this.shadow = true;
}
/**
@@ -331,6 +332,7 @@ export function ChartConfig(
subtitle: this.getSubtitle(),
defaultConfig: self.defaultConfig,
fullHeight: true,
+ shadow: this.shadow,
});
chart.translations = self.translations;
diff --git a/client/charts/styles/charts.scss b/client/charts/styles/charts.scss
index f5c8d11f9..8e6268770 100644
--- a/client/charts/styles/charts.scss
+++ b/client/charts/styles/charts.scss
@@ -238,3 +238,158 @@ $colors: #7cb5ec #434348 #90ed7d #f7a35c #8085e9 #f15c80 #e4d354 #2b908f #f45b5b
.highcharts-data-label-color-9 .highcharts-data-label-box {@extend .highcharts-color-9;}
.highcharts-data-label-color-9.highcharts-data-label text {fill: $gray;}
+.sda-black {
+ fill: $black;
+ stroke: $black;
+ background-color: $black !important;
+}
+
+.sda-gray-darker {
+ fill: $grayDarker;
+ stroke: $grayDarker;
+ background-color: $grayDarker !important;
+}
+
+.sda-gray-dark {
+ fill: $grayDark;
+ stroke: $grayDark;
+ background-color: $grayDark !important;
+}
+
+.sda-gray-medium {
+ fill: $grayMedium;
+ stroke: $grayMedium;
+ background-color: $grayMedium !important;
+}
+
+.sda-gray {
+ fill: $gray;
+ stroke: $gray;
+ background-color: $gray !important;
+}
+
+.sda-gray-neutral {
+ fill: $grayNeutral;
+ stroke: $grayNeutral;
+ background-color: $grayNeutral !important;
+}
+
+.sda-gray-text {
+ fill: $grayText;
+ stroke: $grayText;
+ background-color: $grayText !important;
+}
+
+.sda-gray-light {
+ fill: $grayLight;
+ stroke: $grayLight;
+ background-color: $grayLight !important;
+}
+
+.sda-gray-lighter {
+ fill: $grayLighter;
+ stroke: $grayLighter;
+ background-color: $grayLighter !important;
+}
+
+.sda-white {
+ fill: $white;
+ stroke: $white;
+ background-color: $white !important;
+}
+
+.sda-blue {
+ fill: $sd-blue;
+ stroke: $sd-blue;
+ background-color: $sd-blue !important;
+}
+
+.sda-blue-medium {
+ fill: $sd-blueMedium;
+ stroke: $sd-blueMedium;
+ background-color: $sd-blueMedium !important;
+}
+
+.sda-blue-dark {
+ fill: $sd-blueDark;
+ stroke: $sd-blueDark;
+ background-color: $sd-blueDark !important;
+}
+
+.sda-green {
+ fill: $green;
+ stroke: $green;
+ background-color: $green !important;
+}
+
+.sda-red {
+ fill: $red;
+ stroke: $red;
+ background-color: $red !important;
+}
+
+.sda-yellow {
+ fill: $yellow;
+ stroke: $yellow;
+ background-color: $yellow !important;
+}
+
+.sda-orange {
+ fill: $orange;
+ stroke: $orange;
+ background-color: $orange !important;
+}
+
+.sda-purple {
+ fill: $purple;
+ stroke: $purple;
+ background-color: $purple !important;
+}
+
+.sda-fern-green {
+ fill: $fernGreen;
+ stroke: $fernGreen;
+ background-color: $fernGreen !important;
+}
+
+.sda-old-gold {
+ fill: $oldGold;
+ stroke: $oldGold;
+ background-color: $oldGold !important;
+}
+
+.sda-dark-orange {
+ fill: $darkOrange;
+ stroke: $darkOrange;
+ background-color: $darkOrange !important;
+}
+
+.sda-fire-brick {
+ fill: $fireBrick;
+ stroke: $fireBrick;
+ background-color: $fireBrick !important;
+}
+
+.sda-deep-pink {
+ fill: $deepPink;
+ stroke: $deepPink;
+ background-color: $deepPink !important;
+}
+
+.sda-dark-magenta {
+ fill: $darkMagenta;
+ stroke: $darkMagenta;
+ background-color: $darkMagenta !important;
+}
+
+.sda-dark-violet {
+ fill: $darkViolet;
+ stroke: $darkViolet;
+ background-color: $darkViolet !important;
+}
+
+.sda-navy {
+ fill: $navy;
+ stroke: $navy;
+ background-color: $navy !important;
+}
diff --git a/client/charts/tests/ChartConfig.spec.js b/client/charts/tests/ChartConfig.spec.js
index 65d4ff4f9..2dab52d62 100644
--- a/client/charts/tests/ChartConfig.spec.js
+++ b/client/charts/tests/ChartConfig.spec.js
@@ -106,6 +106,7 @@ describe('chartConfig', () => {
}],
legend: {enabled: false},
tooltip: {
+ enabled: true,
headerFormat: '{point.x}: {point.y}',
pointFormat: '',
},
@@ -120,6 +121,7 @@ describe('chartConfig', () => {
type: 'bar',
xAxis: 0,
}],
+ shadow: true,
...defaultConfig,
});
});
@@ -189,8 +191,10 @@ describe('chartConfig', () => {
legend: {
enabled: true,
title: {text: 'Urgency'},
+ useHTML: true,
},
tooltip: {
+ enabled: true,
headerFormat: '{series.name}/{point.x}: {point.y}',
pointFormat: '',
},
@@ -221,6 +225,7 @@ describe('chartConfig', () => {
stacking: 'normal',
stack: 0,
}],
+ shadow: true,
...defaultConfig,
});
});
diff --git a/client/charts/views/chart-colour-picker.html b/client/charts/views/chart-colour-picker.html
new file mode 100644
index 000000000..699d7882c
--- /dev/null
+++ b/client/charts/views/chart-colour-picker.html
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/client/charts/views/chart-form-options.html b/client/charts/views/chart-form-options.html
index 9563f29d8..5a0515fdc 100644
--- a/client/charts/views/chart-form-options.html
+++ b/client/charts/views/chart-form-options.html
@@ -1,32 +1,32 @@
-
-
diff --git a/client/planning_usage_report/views/planning-usage-report-panel.html b/client/planning_usage_report/views/planning-usage-report-panel.html
index 35c3e15ad..210a9a70b 100644
--- a/client/planning_usage_report/views/planning-usage-report-panel.html
+++ b/client/planning_usage_report/views/planning-usage-report-panel.html
@@ -59,8 +59,12 @@
ng-include="'planning-usage-report-parameters.html'"
>
-
diff --git a/client/production_time_report/views/production-time-report-panel.html b/client/production_time_report/views/production-time-report-panel.html
index c20deb9d4..fcb275602 100644
--- a/client/production_time_report/views/production-time-report-panel.html
+++ b/client/production_time_report/views/production-time-report-panel.html
@@ -71,8 +71,12 @@
data-fields="sourceFilters"
>
-
diff --git a/client/publishing_performance_report/index.js b/client/publishing_performance_report/index.js
index f8be1291b..32062a163 100644
--- a/client/publishing_performance_report/index.js
+++ b/client/publishing_performance_report/index.js
@@ -10,6 +10,7 @@
import * as ctrl from './controllers';
import * as directives from './directives';
+import './widgets';
function cacheIncludedTemplates($templateCache) {
$templateCache.put(
@@ -30,7 +31,9 @@ cacheIncludedTemplates.$inject = ['$templateCache'];
* @packageName analytics.publishing-performance-report
* @description Superdesk analytics generate report of Publishing Performance statistics.
*/
-angular.module('superdesk.analytics.publishing-performance-report', [])
+angular.module('superdesk.analytics.publishing-performance-report',
+ ['superdesk.analytics.publishing-performance-report.widgets']
+)
.controller('PublishingPerformanceReportController', ctrl.PublishingPerformanceReportController)
.directive('sdaPublishingPerformanceReportPreview', directives.PublishingPerformanceReportPreview)
diff --git a/client/publishing_performance_report/views/publishing-performance-report-panel.html b/client/publishing_performance_report/views/publishing-performance-report-panel.html
index 159df39d8..2b29037bc 100644
--- a/client/publishing_performance_report/views/publishing-performance-report-panel.html
+++ b/client/publishing_performance_report/views/publishing-performance-report-panel.html
@@ -70,8 +70,12 @@
data-params="currentParams.params"
>
-
diff --git a/client/publishing_performance_report/widgets/index.js b/client/publishing_performance_report/widgets/index.js
new file mode 100644
index 000000000..45920cb20
--- /dev/null
+++ b/client/publishing_performance_report/widgets/index.js
@@ -0,0 +1,62 @@
+/**
+ * This file is part of Superdesk.
+ *
+ * Copyright 2018 Sourcefabric z.u. and contributors.
+ *
+ * For the full copyright and license information, please see the
+ * AUTHORS and LICENSE files distributed with this source code, or
+ * at https://www.sourcefabric.org/superdesk/license
+ */
+
+import {PublishingActionsWidgetController} from './publishing_actions/controller';
+
+function cacheIncludedTemplates($templateCache) {
+ $templateCache.put(
+ 'publishing-actions-widget.html',
+ require('./publishing_actions/widget.html')
+ );
+ $templateCache.put(
+ 'publishing-actions-widget-settings.html',
+ require('./publishing_actions/settings.html')
+ );
+}
+cacheIncludedTemplates.$inject = ['$templateCache'];
+
+/**
+ * @ngdoc module
+ * @module superdesk.analytics.publishing-performance-report.widgets
+ * @name superdesk.analytics.publishing-performance-report.widgets
+ * @packageName analytics.publishing-performance-report.widgets
+ * @description Superdesk analytics generate report of Publishing Performance statistics.
+ */
+angular.module('superdesk.analytics.publishing-performance-report.widgets', [
+ 'superdesk.apps.dashboard.widgets',
+ 'superdesk.apps.authoring.widgets',
+ 'superdesk.apps.desks',
+ 'superdesk.apps.workspace',
+ 'superdesk.analytics.charts',
+])
+ .controller('PublishingActionsWidgetController', PublishingActionsWidgetController)
+
+ .run(cacheIncludedTemplates)
+
+ .config([
+ 'dashboardWidgetsProvider', 'gettext',
+ function(dashboardWidgetsProvider, gettext) {
+ dashboardWidgetsProvider.addWidget('publishing-actions', {
+ label: gettext('Publishing Actions'),
+ description: gettext('Publishing Actions Widget'),
+ multiple: true,
+ icon: 'signal',
+ max_sizex: 1,
+ max_sizey: 1,
+ sizex: 1,
+ sizey: 1,
+ thumbnail: 'scripts/apps/ingest/ingest-stats-widget/thumbnail.svg',
+ template: 'publishing-actions-widget.html',
+ configurationTemplate: 'publishing-actions-widget-settings.html',
+ custom: true,
+ removeHeader: true,
+ });
+ },
+ ]);
diff --git a/client/publishing_performance_report/widgets/publishing_actions/controller.js b/client/publishing_performance_report/widgets/publishing_actions/controller.js
new file mode 100644
index 000000000..e784019da
--- /dev/null
+++ b/client/publishing_performance_report/widgets/publishing_actions/controller.js
@@ -0,0 +1,265 @@
+import {DATE_FILTERS} from '../../../search/directives/DateFilters';
+import {CHART_FIELDS} from '../../../charts/directives/ChartOptions';
+import {SDChart} from '../../../charts/SDChart';
+import {CHART_COLOURS} from '../../../charts/directives/ChartColourPicker';
+import {getErrorMessage} from '../../../utils';
+
+
+PublishingActionsWidgetController.$inject = [
+ '$scope',
+ 'lodash',
+ 'notify',
+ 'gettext',
+ 'searchReport',
+ 'chartConfig',
+ 'desks',
+ '$interval',
+];
+
+/**
+ * @ngdoc controller
+ * @module superdesk.apps.analytics.publishing-performance-report.widgets
+ * @name PublishingActionsWidgetController
+ * @requires $scope
+ * @requires lodash
+ * @requires notify
+ * @requires gettext
+ * @requires searchReport
+ * @requires chartConfig
+ * @requires desks
+ * @requires $interval
+ * @description Controller for use with the PublishingActions widget and settings views
+ */
+export function PublishingActionsWidgetController(
+ $scope,
+ _,
+ notify,
+ gettext,
+ searchReport,
+ chartConfig,
+ desks,
+ $interval
+) {
+ /**
+ * @ngdoc method
+ * @name PublishingActionsWidgetController#init
+ * @param {Boolean} forSettings - True if init for use with the settings, false for the widget view
+ * @description Initialises the scope/controller for use with widget or settings view
+ */
+ this.init = (forSettings) => {
+ $scope.ready = false;
+
+ // This fixes an issue when a controller is created, deleted and created again quickly
+ // Reduces the chance of multiple api queries happening
+ $scope.$applyAsync(() => (
+ desks.initialize()
+ .then(() => {
+ $scope.currentDesk = desks.getCurrentDesk();
+
+ if (!_.get($scope, 'widget.configuration')) {
+ $scope.widget.configuration = this.getDefaultConfig();
+ }
+
+ if (forSettings) {
+ this.initForSettings();
+ } else {
+ this.initForWidget();
+ }
+
+ $scope.ready = true;
+ })
+ ));
+ };
+
+ /**
+ * @ngdoc method
+ * @name PublishingActionsWidgetController#getDefaultConfig
+ * @return {Object}
+ * @description Returns the default config to use for this widget
+ */
+ this.getDefaultConfig = () => ({
+ dates: {filter: DATE_FILTERS.TODAY},
+ chart: {
+ sort_order: 'desc',
+ title: _.get($scope, 'widget.label', gettext('Publishing Performance')),
+ colours: {
+ published: CHART_COLOURS.GREEN,
+ killed: CHART_COLOURS.RED,
+ corrected: CHART_COLOURS.BLUE,
+ updated: CHART_COLOURS.YELLOW,
+ recalled: CHART_COLOURS.BLACK,
+ },
+ },
+ });
+
+ /**
+ * @ngdoc method
+ * @name PublishingActionsWidgetController#initForSettings
+ * @description Initialise this controller for use with the settings view
+ */
+ this.initForSettings = () => {
+ $scope.chartFields = [
+ CHART_FIELDS.TITLE,
+ CHART_FIELDS.SORT,
+ ];
+
+ $scope.dateFilters = [
+ DATE_FILTERS.TODAY,
+ DATE_FILTERS.THIS_WEEK,
+ DATE_FILTERS.THIS_MONTH,
+ DATE_FILTERS.RANGE,
+ DATE_FILTERS.RELATIVE_DAYS,
+ DATE_FILTERS.RELATIVE,
+ ];
+ };
+
+ /**
+ * @ngdoc method
+ * @name PublishingActionsWidgetController#initForWidget
+ * @description Initialise this controller for use with the widget view
+ */
+ this.initForWidget = () => {
+ /**
+ * @ngdoc property
+ * @name PublishingActionsWidgetController#chartConfig
+ * @type {Object}
+ * @description The config used to send to the sda-chart directive
+ */
+ $scope.chartConfig = null;
+
+ /**
+ * @ngdoc property
+ * @name PublishingActionsWidgetController#interval
+ * @type {Number}
+ * @description Used to cancel the $interval for this widget on destruction
+ */
+ this.interval = null;
+
+ /**
+ * @ngdoc method
+ * @name PublishingActionsWidgetController#runQuery
+ * @param {Object} params - Parameters to pass to the API
+ * @return {Promise
Last Week
Last Month
+ Today
+ This Week
+ This Month
From: {{startDate}}
diff --git a/client/styles/analytics.scss b/client/styles/analytics.scss
index 8218cb5f0..4fb1a8f1e 100644
--- a/client/styles/analytics.scss
+++ b/client/styles/analytics.scss
@@ -324,3 +324,67 @@
background-color: unset !important;
}
}
+
+// Styles copied from client-core for widget styling
+.widget-list {
+ li {
+ &.widget {
+ &.publishing-actions {
+ .thumbnail {
+ background-color: #d62776;
+ }
+ }
+ }
+ }
+}
+
+.widget-detail {
+ .thumbnail-box {
+ &.publishing-actions {
+ background-color: #d62776;
+ }
+ }
+}
+
+.sd-widget {
+ &.publishing-actions {
+ .widget-header {
+ z-index: 2;
+ background: $white;
+ box-sizing: border-box;
+
+ }
+
+ .widget-content {
+ overflow-x: hidden;
+ top: 0;
+ }
+
+ .widget-container__chart {
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ top: 38px;
+ width: 100%;
+ @include transition(all ease 0.2s);
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ &.wrap {
+ left: -100%;
+ right: auto;
+ overflow: hidden;
+ .scroll-shadow {
+ display: none;
+ }
+ .content-list-holder {
+ overflow: hidden !important;
+ }
+ }
+ &.custom-widget {
+ bottom: 50%;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/client/update_time_report/controllers/UpdateTimeReportController.js b/client/update_time_report/controllers/UpdateTimeReportController.js
index 0e4de6a17..22e3e490b 100644
--- a/client/update_time_report/controllers/UpdateTimeReportController.js
+++ b/client/update_time_report/controllers/UpdateTimeReportController.js
@@ -1,6 +1,7 @@
import {DATE_FILTERS} from '../../search/directives/DateFilters';
import {SOURCE_FILTERS} from '../../search/directives/SourceFilters';
import {getErrorMessage} from '../../utils';
+import {CHART_FIELDS} from '../../charts/directives/ChartOptions';
UpdateTimeReportController.$inject = [
'$scope',
@@ -73,6 +74,13 @@ export function UpdateTimeReportController(
SOURCE_FILTERS.PUBLISH_PARS,
];
+ $scope.chartFields = [
+ CHART_FIELDS.TITLE,
+ CHART_FIELDS.SUBTITLE,
+ CHART_FIELDS.SORT,
+ CHART_FIELDS.PAGE_SIZE,
+ ];
+
$scope.form = {submitted: false};
this.initDefaultParams();
diff --git a/client/update_time_report/views/update-time-report-panel.html b/client/update_time_report/views/update-time-report-panel.html
index 0b75bf708..f5cb51f06 100644
--- a/client/update_time_report/views/update-time-report-panel.html
+++ b/client/update_time_report/views/update-time-report-panel.html
@@ -71,8 +71,13 @@
data-fields="sourceFilters"
>
-
diff --git a/client/user_activity_report/views/user-activity-report-panel.html b/client/user_activity_report/views/user-activity-report-panel.html
index 506a70575..bea4f0ad2 100644
--- a/client/user_activity_report/views/user-activity-report-panel.html
+++ b/client/user_activity_report/views/user-activity-report-panel.html
@@ -71,8 +71,12 @@
data-fields="sourceFilters"
>
-
diff --git a/server/analytics/base_report/__init__.py b/server/analytics/base_report/__init__.py
index 2b4123e8b..8ece8efb3 100644
--- a/server/analytics/base_report/__init__.py
+++ b/server/analytics/base_report/__init__.py
@@ -422,6 +422,15 @@ def _es_get_date_filters(self, params):
elif date_filter == 'relative_days':
lt = 'now'
gte = 'now-{}d'.format(relative_days)
+ elif date_filter == 'today':
+ lt = 'now'
+ gte = 'now/d'
+ elif date_filter == 'this_week':
+ lt = 'now'
+ gte = 'now/w'
+ elif date_filter == 'this_month':
+ lt = 'now'
+ gte = 'now/M'
return lt, gte, time_zone
diff --git a/server/analytics/base_report/base_report_test.py b/server/analytics/base_report/base_report_test.py
index 366427158..3697bcc67 100644
--- a/server/analytics/base_report/base_report_test.py
+++ b/server/analytics/base_report/base_report_test.py
@@ -117,14 +117,14 @@ def test_generate_elastic_query_default(self):
'must_not': []
})
- def test_generate_elastic_query_from_date_attributes(self):
+ def test_generate_elastic_query_from_date_object(self):
with self.app.app_context():
# DateFilters - Range
query = self.service.generate_elastic_query({
'params': {
'dates': {
'filter': 'range',
- 'start': '2018-06-30',
+ 'start': '2018-06-01',
'end': '2018-06-30'
}
}
@@ -134,7 +134,7 @@ def test_generate_elastic_query_from_date_attributes(self):
'range': {
'versioncreated': {
'lt': '2018-06-30T23:59:59+1000',
- 'gte': '2018-06-30T00:00:00+1000',
+ 'gte': '2018-06-01T00:00:00+1000',
'time_zone': '+1000'
}
}
@@ -142,10 +142,32 @@ def test_generate_elastic_query_from_date_attributes(self):
'must_not': []
})
- # DateFilters - Yesterday
+ # DateFilters - Day
query = self.service.generate_elastic_query({
- 'params': {'dates': {'filter': 'yesterday'}}
+ 'params': {
+ 'dates': {
+ 'filter': 'day',
+ 'date': '2018-06-30'
+ }
+ }
})
+ self.assert_bool_query(query, {
+ 'must': [{
+ 'range': {
+ 'versioncreated': {
+ 'lt': '2018-06-30T23:59:59+1000',
+ 'gte': '2018-06-30T00:00:00+1000',
+ 'time_zone': '+1000'
+ }
+ }
+ }],
+ 'must_not': []
+ })
+
+ # DateFilters - Yesterday
+ query = self.service.generate_elastic_query(
+ {'params': {'dates': {'filter': 'yesterday'}}}
+ )
self.assert_bool_query(query, {
'must': [{
'range': {
@@ -160,9 +182,9 @@ def test_generate_elastic_query_from_date_attributes(self):
})
# DateFilters - Last Week
- query = self.service.generate_elastic_query({
- 'params': {'dates': {'filter': 'last_week'}}
- })
+ query = self.service.generate_elastic_query(
+ {'params': {'dates': {'filter': 'last_week'}}}
+ )
self.assert_bool_query(query, {
'must': [{
'range': {
@@ -177,9 +199,9 @@ def test_generate_elastic_query_from_date_attributes(self):
})
# DateFilters - Last Month
- query = self.service.generate_elastic_query({
- 'params': {'dates': {'filter': 'last_month'}}
- })
+ query = self.service.generate_elastic_query(
+ {'params': {'dates': {'filter': 'last_month'}}}
+ )
self.assert_bool_query(query, {
'must': [{
'range': {
@@ -193,15 +215,12 @@ def test_generate_elastic_query_from_date_attributes(self):
'must_not': []
})
- def test_generate_elastic_query_from_date_object(self):
- with self.app.app_context():
- # DateFilters - Range
+ # DateFilters - Relative
query = self.service.generate_elastic_query({
'params': {
'dates': {
- 'filter': 'range',
- 'start': '2018-06-30',
- 'end': '2018-06-30'
+ 'filter': 'relative',
+ 'relative': 12
}
}
})
@@ -209,8 +228,8 @@ def test_generate_elastic_query_from_date_object(self):
'must': [{
'range': {
'versioncreated': {
- 'lt': '2018-06-30T23:59:59+1000',
- 'gte': '2018-06-30T00:00:00+1000',
+ 'lt': 'now',
+ 'gte': 'now-12h',
'time_zone': '+1000'
}
}
@@ -218,16 +237,38 @@ def test_generate_elastic_query_from_date_object(self):
'must_not': []
})
- # DateFilters - Yesterday
+ # DateFilters - Relative Days
+ query = self.service.generate_elastic_query({
+ 'params': {
+ 'dates': {
+ 'filter': 'relative_days',
+ 'relative_days': 7
+ }
+ }
+ })
+ self.assert_bool_query(query, {
+ 'must': [{
+ 'range': {
+ 'versioncreated': {
+ 'lt': 'now',
+ 'gte': 'now-7d',
+ 'time_zone': '+1000'
+ }
+ }
+ }],
+ 'must_not': []
+ })
+
+ # DateFilters - Today
query = self.service.generate_elastic_query(
- {'params': {'dates': {'filter': 'yesterday'}}}
+ {'params': {'dates': {'filter': 'today'}}}
)
self.assert_bool_query(query, {
'must': [{
'range': {
'versioncreated': {
- 'lt': 'now/d',
- 'gte': 'now-1d/d',
+ 'lt': 'now',
+ 'gte': 'now/d',
'time_zone': '+1000'
}
}
@@ -235,16 +276,16 @@ def test_generate_elastic_query_from_date_object(self):
'must_not': []
})
- # DateFilters - Last Week
+ # DateFilters - This Week
query = self.service.generate_elastic_query(
- {'params': {'dates': {'filter': 'last_week'}}}
+ {'params': {'dates': {'filter': 'this_week'}}}
)
self.assert_bool_query(query, {
'must': [{
'range': {
'versioncreated': {
- 'lt': 'now/w',
- 'gte': 'now-1w/w',
+ 'lt': 'now',
+ 'gte': 'now/w',
'time_zone': '+1000'
}
}
@@ -252,16 +293,16 @@ def test_generate_elastic_query_from_date_object(self):
'must_not': []
})
- # DateFilters - Last Month
+ # DateFilters - This Month
query = self.service.generate_elastic_query(
- {'params': {'dates': {'filter': 'last_month'}}}
+ {'params': {'dates': {'filter': 'this_month'}}}
)
self.assert_bool_query(query, {
'must': [{
'range': {
'versioncreated': {
- 'lt': 'now/M',
- 'gte': 'now-1M/M',
+ 'lt': 'now',
+ 'gte': 'now/M',
'time_zone': '+1000'
}
}
diff --git a/server/analytics/publishing_performance_report/publishing_performance_report.py b/server/analytics/publishing_performance_report/publishing_performance_report.py
index e018df1ed..2cbbd7628 100644
--- a/server/analytics/publishing_performance_report/publishing_performance_report.py
+++ b/server/analytics/publishing_performance_report/publishing_performance_report.py
@@ -24,6 +24,7 @@ class PublishingPerformanceReportResource(Resource):
class PublishingPerformanceReportService(BaseReportService):
+ repos = ['published', 'archived']
aggregations = {
'source': {
'terms': {
@@ -104,14 +105,11 @@ def generate_report(self, docs, args):
"""
agg_buckets = self.get_aggregation_buckets(getattr(docs, 'hits'), ['parent'])
+ states = ['killed', 'corrected', 'updated', 'published', 'recalled']
+
report = {
'groups': {},
- 'subgroups': {
- 'killed': 0,
- 'corrected': 0,
- 'updated': 0,
- 'published': 0
- }
+ 'subgroups': {state: 0 for state in states}
}
for parent in agg_buckets.get('parent') or []:
@@ -120,12 +118,7 @@ def generate_report(self, docs, args):
if not parent_key:
continue
- report['groups'][parent_key] = {
- 'killed': 0,
- 'corrected': 0,
- 'updated': 0,
- 'published': 0,
- }
+ report['groups'][parent_key] = {state: 0 for state in states}
no_rewrite_of = (parent.get('no_rewrite_of') or {}).get('state') or {}
rewrite_of = (parent.get('rewrite_of') or {}).get('state') or {}
@@ -133,32 +126,27 @@ def generate_report(self, docs, args):
for child in no_rewrite_of.get('buckets') or []:
state_key = child.get('key')
- if not state_key:
+ if not state_key or state_key not in states:
continue
doc_count = child.get('doc_count') or 0
- if state_key == 'published':
- report['groups'][parent_key]['published'] += doc_count
- report['subgroups']['published'] += doc_count
- elif state_key == 'killed':
- report['groups'][parent_key]['killed'] += doc_count
- report['subgroups']['killed'] += doc_count
+ report['groups'][parent_key][state_key] += doc_count
+ report['subgroups'][state_key] += doc_count
for child in rewrite_of.get('buckets') or []:
state_key = child.get('key')
- if not state_key:
+ if not state_key or state_key not in states:
continue
+ if state_key == 'published':
+ state_key = 'updated'
+
doc_count = child.get('doc_count') or 0
- if state_key == 'corrected':
- report['groups'][parent_key]['corrected'] += doc_count
- report['subgroups']['corrected'] += doc_count
- elif state_key == 'published':
- report['groups'][parent_key]['updated'] += doc_count
- report['subgroups']['updated'] += doc_count
+ report['groups'][parent_key][state_key] += doc_count
+ report['subgroups'][state_key] += doc_count
return report