Compare commits

..

5 Commits

Author SHA1 Message Date
Albert Backenhof
b5d9633496 Replaced semaphores with Conditional Coloring
-Was unclear what the two semaphores did and meant.
 Also, Concept Semaphores didn't seem to work.

Issue: DEB-177
2019-04-11 14:40:06 +02:00
Albert Backenhof
c8cd78ae5f Merge pull request #37 from qlik-oss/DEB-156/OnlyTwoDims
Moved design dimension to Appearance->Table format
2019-04-10 12:14:20 +02:00
Albert Backenhof
a5bc3ecd1b Moved design dimension to Appearance->Table format
-According to the documentation it should only be
 possible to set two dimensions. Previously you
 were able to set three, where the third one was
 used for styling. This wasn't obvious to the user
 though. Now, the design field is set under
 Appearance -> Table format.

Issue: DEB-156
2019-04-10 12:12:08 +02:00
Albert Backenhof
bec68e7cd4 Merge pull request #39 from qlik-oss/DEB-159/Props
Changed order of properties and added About
2019-04-10 12:05:15 +02:00
Albert Backenhof
cac3fabb2f Changed order of properties and added About
Issue: DEB-159
2019-04-10 08:13:24 +02:00
10 changed files with 315 additions and 344 deletions

View File

@@ -54,21 +54,17 @@ class DataCell extends React.PureComponent {
formattedMeasurementValue = formatMeasurementValue(measurement, styling);
}
const { semaphoreColors, semaphoreColors: { fieldsToApplyTo } } = styling;
const isValidSemaphoreValue = !styleBuilder.hasComments() && !isNaN(measurement.value);
const dimension1Row = measurement.parents.dimension1.elementNumber;
const isSpecifiedMetricField = fieldsToApplyTo.metricsSpecificFields.indexOf(dimension1Row) !== -1;
const shouldHaveSemaphoreColors = (fieldsToApplyTo.applyToMetric || isSpecifiedMetricField);
if (isValidSemaphoreValue && shouldHaveSemaphoreColors) {
const { backgroundColor, color } = getSemaphoreColors(measurement, semaphoreColors);
cellStyle = {
...styleBuilder.getStyle(),
backgroundColor,
color,
fontFamily: styling.options.fontFamily,
paddingLeft: '5px',
textAlign: textAlignment
};
const { conditionalColoring } = styling;
if (conditionalColoring.enabled) {
const isValidConditionalColoringValue = !styleBuilder.hasComments() && !isNaN(measurement.value);
const isSpecifiedRow =
conditionalColoring.rows.indexOf(measurement.parents.dimension1.header) !== -1;
const shouldHaveConditionalColoring = conditionalColoring.colorAllRows || isSpecifiedRow;
if (isValidConditionalColoringValue && shouldHaveConditionalColoring) {
const { color, textColor } = getConditionalColor(measurement, conditionalColoring);
cellStyle.backgroundColor = color.color;
cellStyle.color = textColor.color;
}
}
let cellClass = 'grid-cells';
@@ -176,12 +172,12 @@ function formatMeasurementValue (measurement, styling) {
return formattedMeasurementValue;
}
function getSemaphoreColors (measurement, semaphoreColors) {
if (measurement.value < semaphoreColors.status.critical) {
return semaphoreColors.statusColors.critical;
function getConditionalColor (measurement, conditionalColoring) {
if (measurement.value < conditionalColoring.threshold.poor) {
return conditionalColoring.colors.poor;
}
if (measurement.value < semaphoreColors.status.medium) {
return semaphoreColors.statusColors.medium;
if (measurement.value < conditionalColoring.threshold.fair) {
return conditionalColoring.colors.fair;
}
return semaphoreColors.statusColors.normal;
return conditionalColoring.colors.good;
}

View File

@@ -6,7 +6,7 @@ function createCube (definition, app) {
});
}
async function buildDataCube (originCubeDefinition, dimensionIndexes, app) {
async function buildDataCube (originCubeDefinition, hasTwoDimensions, app) {
const cubeDefinition = {
...originCubeDefinition,
qInitialDataFetch: [
@@ -15,62 +15,18 @@ async function buildDataCube (originCubeDefinition, dimensionIndexes, app) {
qWidth: 10
}
],
qDimensions: [originCubeDefinition.qDimensions[dimensionIndexes.dimension1]],
qDimensions: [originCubeDefinition.qDimensions[0]],
qMeasures: originCubeDefinition.qMeasures
};
if (dimensionIndexes.dimension2) {
cubeDefinition.qDimensions.push(originCubeDefinition.qDimensions[dimensionIndexes.dimension2]);
if (hasTwoDimensions) {
cubeDefinition.qDimensions.push(originCubeDefinition.qDimensions[1]);
}
const cube = await createCube(cubeDefinition, app);
return cube.qHyperCube.qDataPages[0].qMatrix;
}
async function buildDesignCube (originCubeDefinition, dimensionIndexes, app) {
if (!dimensionIndexes.design) {
return null;
}
const cube = await createCube({
qInitialDataFetch: [
{
qHeight: 1000,
qWidth: 1
}
],
qDimensions: [originCubeDefinition.qDimensions[dimensionIndexes.design]]
}, app);
return cube.qHyperCube.qDataPages[0].qMatrix;
}
const STYLE_SEPARATOR_COUNT = 7;
function findDesignDimension (qMatrix) {
return qMatrix[0].map(entry => (entry.qText.match(/;/g) || []).length).indexOf(STYLE_SEPARATOR_COUNT);
}
function getDimensionIndexes (dimensionsInformation, designDimensionIndex) {
const hasDesign = designDimensionIndex !== -1;
const nonDesignDimensionCount = hasDesign ? dimensionsInformation.length - 1 : dimensionsInformation.length;
const dimension1 = designDimensionIndex === 0 ? 1 : 0;
let dimension2 = false;
if (nonDesignDimensionCount === 2) {
dimension2 = hasDesign && designDimensionIndex < 2 ? 2 : 1;
}
const design = hasDesign && designDimensionIndex;
const firstMeasurementIndex = dimensionsInformation.length;
return {
design,
dimension1,
dimension2,
firstMeasurementIndex
};
}
export async function initializeCubes ({ component, layout }) {
export async function initializeDataCube (component, layout) {
const app = qlik.currApp(component);
const designDimensionIndex = findDesignDimension(layout.qHyperCube.qDataPages[0].qMatrix);
const dimensionsInformation = layout.qHyperCube.qDimensionInfo;
const dimensionIndexes = getDimensionIndexes(dimensionsInformation, designDimensionIndex);
let properties;
if (component.backendApi.isSnapshot) {
@@ -80,12 +36,24 @@ export async function initializeCubes ({ component, layout }) {
properties = await component.backendApi.getProperties();
}
const originCubeDefinition = properties.qHyperCubeDef;
const designCube = await buildDesignCube(originCubeDefinition, dimensionIndexes, app);
const dataCube = await buildDataCube(originCubeDefinition, dimensionIndexes, app);
return {
design: designCube,
data: dataCube
};
return buildDataCube(
properties.qHyperCubeDef, layout.qHyperCube.qDimensionInfo.length === 2, app);
}
export function initializeDesignList (component, layout) {
if (!layout.stylingfield) {
return null;
}
return new Promise(resolve => {
const app = qlik.currApp(component);
const stylingField = app.field(layout.stylingfield);
const listener = function () {
const data = stylingField.rows.map(row => row.qText);
stylingField.OnData.unbind(listener);
resolve(data);
};
stylingField.OnData.bind(listener);
stylingField.getData();
});
}

View File

@@ -1,96 +0,0 @@
const conceptSemaphores = {
items: {
AllConcepts: {
component: 'switch',
defaultValue: true,
label: 'All concepts affected',
options: [
{
label: 'On',
value: true
},
{
label: 'Off',
value: false
}
],
ref: 'allsemaphores',
type: 'boolean'
},
ConceptsAffected1: {
defaultValue: '',
ref: 'conceptsemaphore1',
show: data => !data.allsemaphores,
translation: 'Concept 1',
type: 'string'
},
ConceptsAffected2: {
defaultValue: '',
ref: 'conceptsemaphore2',
show: data => !data.allsemaphores,
translation: 'Concept 2',
type: 'string'
},
ConceptsAffected3: {
defaultValue: '',
ref: 'conceptsemaphore3',
show: data => !data.allsemaphores,
translation: 'Concept 3',
type: 'string'
},
ConceptsAffected4: {
defaultValue: '',
ref: 'conceptsemaphore4',
show: data => !data.allsemaphores,
translation: 'Concept 4',
type: 'string'
},
ConceptsAffected5: {
defaultValue: '',
ref: 'conceptsemaphore5',
show: data => !data.allsemaphores,
translation: 'Concept 5',
type: 'string'
},
ConceptsAffected6: {
defaultValue: '',
ref: 'conceptsemaphore6',
show: data => !data.allsemaphores,
translation: 'Concept 6',
type: 'string'
},
ConceptsAffected7: {
defaultValue: '',
ref: 'conceptsemaphore7',
show: data => !data.allsemaphores,
translation: 'Concept 7',
type: 'string'
},
ConceptsAffected8: {
defaultValue: '',
ref: 'conceptsemaphore8',
show: data => !data.allsemaphores,
translation: 'Concept 8',
type: 'string'
},
ConceptsAffected9: {
defaultValue: '',
ref: 'conceptsemaphore9',
show: data => !data.allsemaphores,
translation: 'Concept 9',
type: 'string'
},
// eslint-disable-next-line sort-keys
ConceptsAffected10: {
defaultValue: '',
ref: 'conceptsemaphore10',
show: data => !data.allsemaphores,
translation: 'Concept 10',
type: 'string'
}
},
label: 'Concept Semaphores',
type: 'items'
};
export default conceptSemaphores;

View File

@@ -0,0 +1,169 @@
const conditionalColoring = {
type: 'items',
label: 'Color by condition',
items: {
Enabled: {
ref: 'conditionalcoloring.enabled',
type: 'boolean',
label: 'Enabled',
component: 'switch',
defaultValue: false,
options: [
{
value: true,
label: 'On'
},
{
value: false,
label: 'Off'
}
]
},
ColorAllRows: {
ref: 'conditionalcoloring.colorall',
type: 'boolean',
label: 'Color all rows by condition',
component: 'switch',
defaultValue: true,
options: [
{
value: true,
label: 'All rows'
},
{
value: false,
label: 'Specified rows'
}
],
show (data) {
return data.conditionalcoloring.enabled;
}
},
Rows: {
type: 'array',
ref: 'conditionalcoloring.rows',
label: 'Rows to color',
itemTitleRef: function (data) {
return data.rowname;
},
allowAdd: true,
allowRemove: true,
addTranslation: 'Add row to color',
items: {
Row: {
ref: 'rowname',
label: 'Name of row',
type: 'string',
defaultValue: ''
}
},
show (data) {
return data.conditionalcoloring.enabled && !data.conditionalcoloring.colorall;
}
},
ThresholdPoor: {
ref: 'conditionalcoloring.threshold_poor',
translation: 'Poor is less than',
type: 'number',
defaultValue: -0.1,
show (data) {
return data.conditionalcoloring.enabled;
}
},
ColorPoor: {
ref: 'conditionalcoloring.color_poor',
label: 'Poor color fill',
type: 'object',
component: 'color-picker',
dualOutput: true,
defaultValue: {
index: 10,
color: '#f93f17'
},
show (data) {
return data.conditionalcoloring.enabled;
}
},
TextColorPoor: {
ref: 'conditionalcoloring.textcolor_poor',
label: 'Poor text color',
type: 'object',
component: 'color-picker',
dualOutput: true,
defaultValue: {
index: 1,
color: '#ffffff'
},
show (data) {
return data.conditionalcoloring.enabled;
}
},
ThresholdFair: {
ref: 'conditionalcoloring.threshold_fair',
translation: 'Fair is less than',
type: 'number',
defaultValue: 0,
show (data) {
return data.conditionalcoloring.enabled;
}
},
ColorFair: {
ref: 'conditionalcoloring.color_fair',
label: 'Fair color fill',
type: 'object',
component: 'color-picker',
dualOutput: true,
defaultValue: {
index: 8,
color: '#ffcf02'
},
show (data) {
return data.conditionalcoloring.enabled;
}
},
TextColorFair: {
ref: 'conditionalcoloring.textcolor_fair',
label: 'Fair text color',
type: 'object',
component: 'color-picker',
dualOutput: true,
defaultValue: {
index: 15,
color: '#000000'
},
show (data) {
return data.conditionalcoloring.enabled;
}
},
ColorGood: {
ref: 'conditionalcoloring.color_good',
label: 'Good color fill',
type: 'object',
component: 'color-picker',
dualOutput: true,
defaultValue: {
index: 3,
color: '#276e27'
},
show (data) {
return data.conditionalcoloring.enabled;
}
},
TextColorGood: {
ref: 'conditionalcoloring.textcolor_good',
label: 'Good text color',
type: 'object',
component: 'color-picker',
dualOutput: true,
defaultValue: {
index: 1,
color: '#ffffff'
},
show (data) {
return data.conditionalcoloring.enabled;
}
}
}
};
export default conditionalColoring;

View File

@@ -1,8 +1,7 @@
import pagination from './pagination';
import header from './header';
import tableFormat from './table-format';
import conceptSemaphores from './concept-semaphores';
import metricSemaphores from './metric-semaphores';
import conditionalColoring from './conditional-coloring';
const definition = {
component: 'accordion',
@@ -18,18 +17,37 @@ const definition = {
},
uses: 'data'
},
sorting: {
uses: 'sorting'
},
settings: {
items: {
ConceptSemaphores: conceptSemaphores,
Formatted: tableFormat,
Header: header,
MetricSemaphores: metricSemaphores,
ConditionalColoring: conditionalColoring,
Pagination: pagination
},
uses: 'settings'
},
sorting: {
uses: 'sorting'
about: {
component: 'items',
label: 'About',
items: {
header: {
label: 'P&L pivot',
style: 'header',
component: 'text'
},
paragraph1: {
label: `P&L pivot is a Qlik Sense extension which allows you to display Profit & Loss
reporting with color and font customizations.`,
component: 'text'
},
paragraph2: {
label: 'P&L pivot is based upon an extension created by Ivan Felipe Asensio.',
component: 'text'
}
}
}
},
type: 'items'

View File

@@ -1,112 +0,0 @@
const metricSemaphores = {
type: 'items',
label: 'Metric Semaphores',
items: {
AllMetrics: {
ref: 'allmetrics',
type: 'boolean',
component: 'switch',
label: 'All metrics affected',
options: [
{
value: true,
label: 'On'
},
{
value: false,
label: 'Off'
}
],
defaultValue: false
},
MetricsAffected: {
ref: 'metricssemaphore',
translation: 'Metrics affected (1,2,4,...)',
type: 'string',
defaultValue: '-',
show (data) {
return !data.allmetrics;
}
},
MetricStatus1: {
ref: 'metricsstatus1',
translation: 'Critic is less than',
type: 'number',
defaultValue: -0.1
},
ColorStatus1: {
ref: 'colorstatus1',
label: 'Critic Color Fill',
type: 'object',
component: 'color-picker',
dualOutput: true,
defaultValue: {
index: 8,
color: '#f93f17'
}
},
ColorStatus1Text: {
ref: 'colorstatus1text',
label: 'Critic Color Text',
type: 'object',
component: 'color-picker',
dualOutput: true,
defaultValue: {
index: 11,
color: '#ffffff'
}
},
MetricStatus2: {
ref: 'metricsstatus2',
translation: 'Medium is less than',
type: 'number',
defaultValue: 0
},
ColorStatus2: {
ref: 'colorstatus2',
label: 'Medium Color Fill',
type: 'object',
component: 'color-picker',
dualOutput: true,
defaultValue: {
index: 9,
color: '#ffcf02'
}
},
ColorStatus2Text: {
ref: 'colorstatus2text',
label: 'Medium Color Text',
type: 'object',
component: 'color-picker',
dualOutput: true,
defaultValue: {
index: 12,
color: '#000000'
}
},
ColorStatus3: {
ref: 'colorstatus3',
label: 'Success Color Fill',
type: 'object',
component: 'color-picker',
dualOutput: true,
defaultValue: {
index: 10,
color: '#276e27'
}
},
ColorStatus3Text: {
ref: 'colorstatus3text',
label: 'Success Color Text',
type: 'object',
component: 'color-picker',
dualOutput: true,
defaultValue: {
index: 11,
color: '#ffffff'
}
}
}
};
export default metricSemaphores;

View File

@@ -1,12 +1,53 @@
const qlik = window.require('qlik');
// fixes case for when there are 3 dimensions, missies the case with 1 design dimension and 1 data dimension
function hasDesignDimension (data) {
return data.qHyperCubeDef.qDimensions.length > 2;
}
function getFieldList () {
return new Promise(function (resolve) {
const app = qlik.currApp();
app.getList('FieldList').then(function (model) {
// Close the model to prevent any updates.
app.destroySessionObject(model.layout.qInfo.qId);
// This is a bit iffy, might be smarter to reject and handle empty lists on the props instead.
if (!model.layout.qFieldList.qItems) {
return resolve([]);
}
// Resolve an array with master objects.
return resolve(model.layout.qFieldList.qItems.map(function (item) {
return {
value: item.qName,
label: item.qName
};
}));
});
});
}
const tableFormat = {
type: 'items',
label: 'Table Format',
items: {
StylingField: {
ref: 'stylingfield',
disabledRef: '',
type: 'string',
component: 'dropdown',
label: 'Style with field',
options: function () {
return getFieldList().then(function (items) {
items.unshift(
{
value: '',
label: 'None'
});
return items;
});
}
},
IndentBool: {
ref: 'indentbool',
type: 'boolean',

View File

@@ -107,15 +107,16 @@ function generateMatrixCell ({ cell, dimension1Information, dimension2Informatio
}
let lastRow = 0;
function generateDataSet (component, dimensionsInformation, measurementsInformation, cubes) {
function generateDataSet (
component, dimensionsInformation, measurementsInformation, dataCube) {
const measurements = generateMeasurements(measurementsInformation);
let dimension1 = [];
let dimension2 = [];
const measurements = generateMeasurements(measurementsInformation);
let matrix = [];
const hasDesignDimension = cubes.design;
const hasSecondDimension = hasDesignDimension ? dimensionsInformation.length > 2 : dimensionsInformation.length > 1;
cubes.data.forEach(row => {
const hasSecondDimension = dimensionsInformation.length > 1;
dataCube.forEach(row => {
lastRow += 1;
const dimension1Entry = generateDimensionEntry(dimensionsInformation[0], row[0]);
dimension1.push(dimension1Entry);
@@ -200,7 +201,7 @@ function generateDataSet (component, dimensionsInformation, measurementsInformat
};
}
function initializeTransformed ({ $element, component, cubes, layout }) {
function initializeTransformed ({ $element, component, dataCube, designList, layout }) {
const dimensionsInformation = component.backendApi.getDimensionInfos();
const measurementsInformation = component.backendApi.getMeasureInfos();
const dimensionCount = layout.qHyperCube.qDimensionInfo.length;
@@ -211,19 +212,18 @@ function initializeTransformed ({ $element, component, cubes, layout }) {
dimension2,
measurements,
matrix
} = generateDataSet(component, dimensionsInformation, measurementsInformation, cubes);
} = generateDataSet(component, dimensionsInformation, measurementsInformation, dataCube);
const customSchemaBasic = [];
const customSchemaFull = [];
let customHeadersCount = 0;
if (cubes.design) {
const allTextLines = cubes.design.map(entry => entry[0].qText);
const headers = allTextLines[0].split(';');
if (designList && designList.length > 0) {
const headers = designList[0].split(';');
customHeadersCount = headers.length;
for (let lineNumber = 0; lineNumber < allTextLines.length; lineNumber += 1) {
for (let lineNumber = 0; lineNumber < designList.length; lineNumber += 1) {
customSchemaFull[lineNumber] = new Array(headers.length);
const data = allTextLines[lineNumber].split(';');
const data = designList[lineNumber].split(';');
if (data.length === headers.length) {
for (let headerIndex = 0; headerIndex < headers.length; headerIndex += 1) {
@@ -267,7 +267,7 @@ function initializeTransformed ({ $element, component, cubes, layout }) {
count: customHeadersCount,
full: customSchemaFull
},
hasCustomFileStyle: Boolean(cubes.design),
hasCustomFileStyle: Boolean(designList),
headerOptions: {
alignment: getAlignment(layout.HeaderAlign),
colorSchema: layout.HeaderColorSchema.color,
@@ -282,39 +282,26 @@ function initializeTransformed ({ $element, component, cubes, layout }) {
fontSizeAdjustment: getFontSizeAdjustment(layout.lettersize),
textAlignment: layout.cellTextAlignment
},
semaphoreColors: {
fieldsToApplyTo: {
applyToAll: layout.allsemaphores,
applyToMetric: layout.allmetrics,
specificFields: [
layout.conceptsemaphore1,
layout.conceptsemaphore2,
layout.conceptsemaphore3,
layout.conceptsemaphore4,
layout.conceptsemaphore5,
layout.conceptsemaphore6,
layout.conceptsemaphore7,
layout.conceptsemaphore9,
layout.conceptsemaphore10
],
metricsSpecificFields: layout.metricssemaphore.split(',').map(entry => Number(entry))
conditionalColoring: {
enabled: layout.conditionalcoloring.enabled,
colorAllRows: layout.conditionalcoloring.colorall,
rows: layout.conditionalcoloring.rows.map(row => row.rowname),
threshold: {
poor: layout.conditionalcoloring.threshold_poor,
fair: layout.conditionalcoloring.threshold_fair
},
status: {
critical: layout.metricsstatus1,
medium: layout.metricsstatus2
},
statusColors: {
critical: {
backgroundColor: layout.colorstatus1.color,
color: layout.colorstatus1text.color
colors: {
poor: {
color: layout.conditionalcoloring.color_poor,
textColor: layout.conditionalcoloring.textcolor_poor
},
medium: {
backgroundColor: layout.colorstatus2.color,
color: layout.colorstatus2text.color
fair: {
color: layout.conditionalcoloring.color_fair,
textColor: layout.conditionalcoloring.textcolor_fair
},
normal: {
backgroundColor: layout.colorstatus3.color,
color: layout.colorstatus3text.color
good: {
color: layout.conditionalcoloring.color_good,
textColor: layout.conditionalcoloring.textcolor_good
}
}
},

View File

@@ -2,25 +2,24 @@ import initializeStore from './store';
import React from 'react';
import ReactDOM from 'react-dom';
import Root from './root.jsx';
import { initializeCubes } from './dataset';
import { initializeDataCube, initializeDesignList } from './dataset';
export default async function paint ($element, layout, component) {
const cubes = await initializeCubes({
component,
layout
});
const dataCube = await initializeDataCube(component, layout);
const designList = await initializeDesignList(component, layout);
const state = await initializeStore({
$element,
component,
cubes,
dataCube,
designList,
layout
});
const editmodeClass = component.inAnalysisState() ? '' : 'edit-mode';
const jsx = (
<Root
editmodeClass={editmodeClass}
qlik={component}
state={state}
editmodeClass={editmodeClass}
/>
);

View File

@@ -1,10 +1,11 @@
import initializeTransformed from './initialize-transformed';
async function initialize ({ $element, layout, component, cubes }) {
async function initialize ({ $element, layout, component, dataCube, designList }) {
const transformedProperties = await initializeTransformed({
$element,
component,
cubes,
dataCube,
designList,
layout
});