diff --git a/README.md b/README.md
index 3e679a322..dc2221024 100644
--- a/README.md
+++ b/README.md
@@ -29,6 +29,17 @@ Create a Google Sheet. Give it at least the below column headers, and put in the
| Apache Kylin | assess | platforms | TRUE | Apache Kylin is an open source analytics solution ... |
| JSF | hold | languages & frameworks | FALSE | We continue to see teams run into trouble using JSF ... |
+### Want to show blip movement information?
+
+If you want to show movement of blips, add the optional column `status` to your dataset.
+
+This column accepts the following case-insensitive values :
+
+- `New` - appearing on the radar for the first time
+- `Moved In` - moving towards the center of the radar
+- `Moved Out` - moving towards the edge of the radar
+- `No Change` - no change in position
+
### Sharing the sheet
- In Google Sheets, click on "Share".
diff --git a/spec/graphing/blips-spec.js b/spec/graphing/blips-spec.js
index 8407e1463..8666c6594 100644
--- a/spec/graphing/blips-spec.js
+++ b/spec/graphing/blips-spec.js
@@ -187,12 +187,77 @@ describe('Blips', function () {
blipText: () => '12 New Blips',
name: 'blip1',
isNew: () => true,
+ status: () => null,
}
const actual = blipAssistiveText(blip)
expect(actual).toEqual('`ring1 ring, group of 12 New Blips')
})
+ it('should return correct assistive text for new blip', function () {
+ const blip = {
+ isGroup: () => false,
+ ring: () => {
+ return {
+ name: () => 'Trial',
+ }
+ },
+ name: () => 'Some cool tech',
+ status: () => 'New',
+ }
+
+ const actual = blipAssistiveText(blip)
+ expect(actual).toEqual('Trial ring, Some cool tech, New.')
+ })
+
+ it('should return correct assistive text for existing blip', function () {
+ const blip = {
+ isGroup: () => false,
+ ring: () => {
+ return {
+ name: () => 'Trial',
+ }
+ },
+ name: () => 'Some cool tech',
+ status: () => 'No change',
+ }
+
+ const actual = blipAssistiveText(blip)
+ expect(actual).toEqual('Trial ring, Some cool tech, No change.')
+ })
+
+ it('should return correct assistive text for moved in blip', function () {
+ const blip = {
+ isGroup: () => false,
+ ring: () => {
+ return {
+ name: () => 'Trial',
+ }
+ },
+ name: () => 'Some cool tech',
+ status: () => 'Moved in',
+ }
+
+ const actual = blipAssistiveText(blip)
+ expect(actual).toEqual('Trial ring, Some cool tech, Moved in.')
+ })
+
+ it('should return correct assistive text for moved out blip', function () {
+ const blip = {
+ isGroup: () => false,
+ ring: () => {
+ return {
+ name: () => 'Trial',
+ }
+ },
+ name: () => 'Some cool tech',
+ status: () => 'Moved out',
+ }
+
+ const actual = blipAssistiveText(blip)
+ expect(actual).toEqual('Trial ring, Some cool tech, Moved out.')
+ })
+
it('should return group blip with appropriate values', function () {
const ringBlips = mockRingBlips(20)
const groupBlip = createGroupBlip(ringBlips, 'New', { name: () => 'ring1' }, 'first')
diff --git a/spec/models/blip-spec.js b/spec/models/blip-spec.js
index 67f3c944d..d8e4654d1 100644
--- a/spec/models/blip-spec.js
+++ b/spec/models/blip-spec.js
@@ -14,7 +14,7 @@ describe('Blip', function () {
})
it('has a topic', function () {
- blip = new Blip('My Blip', new Ring('My Ring'), true, 'topic', 'description')
+ blip = new Blip('My Blip', new Ring('My Ring'), true, null, 'topic', 'description')
expect(blip.topic()).toEqual('topic')
})
@@ -24,7 +24,7 @@ describe('Blip', function () {
})
it('has a description', function () {
- blip = new Blip('My Blip', new Ring('My Ring'), true, 'topic', 'description')
+ blip = new Blip('My Blip', new Ring('My Ring'), true, null, 'topic', 'description')
expect(blip.description()).toEqual('description')
})
@@ -56,17 +56,41 @@ describe('Blip', function () {
})
it('is new', function () {
- blip = new Blip('My Blip', new Ring('My Ring'), true)
+ blip = new Blip('My Blip', new Ring('My Ring'), true, null, 'My Topic', 'My Description')
expect(blip.isNew()).toBe(true)
})
it('is not new', function () {
- blip = new Blip('My Blip', new Ring('My Ring'), false)
+ blip = new Blip('My Blip', new Ring('My Ring'), false, null, 'My Topic', 'My Description')
expect(blip.isNew()).toBe(false)
})
+ it('status is new', function () {
+ blip = new Blip('My Blip', new Ring('My Ring'), null, 'new', 'My Topic', 'My Description')
+
+ expect(blip.isNew()).toBe(true)
+ })
+
+ it('status has moved in', function () {
+ blip = new Blip('My Blip', new Ring('My Ring'), null, 'Moved In', 'My Topic', 'My Description')
+
+ expect(blip.hasMovedIn()).toBe(true)
+ })
+
+ it('status has moved out', function () {
+ blip = new Blip('My Blip', new Ring('My Ring'), null, 'Moved Out', 'My Topic', 'My Description')
+
+ expect(blip.hasMovedOut()).toBe(true)
+ })
+
+ it('status has no change', function () {
+ blip = new Blip('My Blip', new Ring('My Ring'), null, 'No Change', 'My Topic', 'My Description')
+
+ expect(blip.hasNoChange()).toBe(true)
+ })
+
it('has false as default value for isGroup', function () {
expect(blip.isGroup()).toEqual(false)
})
diff --git a/spec/util/inputSanitizer-spec.js b/spec/util/inputSanitizer-spec.js
index 60a1a0118..cd968f182 100644
--- a/spec/util/inputSanitizer-spec.js
+++ b/spec/util/inputSanitizer-spec.js
@@ -107,6 +107,7 @@ describe('Input Santizer for Protected sheet', function () {
ring: '',
quadrant: '',
isNew: '',
+ status: '',
})
})
})
diff --git a/src/common.js b/src/common.js
index bed7cdf5e..363b731ce 100644
--- a/src/common.js
+++ b/src/common.js
@@ -6,7 +6,9 @@ require('./images/search-logo-2x.svg')
require('./images/banner-image-mobile.jpg')
require('./images/banner-image-desktop.jpg')
require('./images/new.svg')
+require('./images/moved.svg')
require('./images/existing.svg')
+require('./images/no-change.svg')
require('./images/arrow-icon.svg')
require('./images/first-quadrant-btn-bg.svg')
require('./images/second-quadrant-btn-bg.svg')
diff --git a/src/graphing/blips.js b/src/graphing/blips.js
index 0182ebf30..ac25a5fa3 100644
--- a/src/graphing/blips.js
+++ b/src/graphing/blips.js
@@ -106,7 +106,7 @@ function findBlipCoordinates(blip, minRadius, maxRadius, startAngle, allBlipCoor
function blipAssistiveText(blip) {
return blip.isGroup()
? `\`${blip.ring().name()} ring, group of ${blip.blipText()}`
- : `${blip.ring().name()} ring, ${blip.name()}, ${blip.isNew() ? 'new' : 'existing'} blip.`
+ : `${blip.ring().name()} ring, ${blip.name()}, ${blip.status()}.`
}
function addOuterCircle(parentSvg, order, scale = 1) {
parentSvg
@@ -120,6 +120,66 @@ function addOuterCircle(parentSvg, order, scale = 1) {
.style('transform', `scale(${scale})`)
}
+function addMovedInLine(parentSvg, order, scale = 1) {
+ let path
+
+ switch (order) {
+ case 'first':
+ path =
+ 'M16.5 34.44c0-.86.7-1.56 1.56-1.56c8.16 0 14.8-6.64 14.8-14.8c0-.86.7-1.56 1.56-1.56c.86 0 1.56.7 1.56 1.56C36 27.96 27.96 36 18.07 36C17.2 36 16.5 35.3 16.5 34.44z'
+ break
+ case 'second':
+ path =
+ 'M16.5 1.56c0 .86.7 1.56 1.56 1.56c8.16 0 14.8 6.64 14.8 14.8c0 .86.7 1.56 1.56 1.56c.86 0 1.56-.7 1.56-1.56C36 8.04 27.96 0 18.07 0C17.2 0 16.5.7 16.5 1.56z'
+ break
+ case 'third':
+ path =
+ 'M19.5 34.44c0-.86-.7-1.56-1.56-1.56c-8.16 0-14.8-6.64-14.8-14.8c0-.86-.7-1.56-1.56-1.56S0 17.2 0 18.07C0 27.96 8.04 36 17.93 36C18.8 36 19.5 35.3 19.5 34.44z'
+ break
+ case 'fourth':
+ path =
+ 'M19.5 1.56c0 0.86-0.7 1.56-1.56 1.56c-8.16 0-14.8 6.64-14.8 14.8c0 0.86-0.7 1.56-1.56 1.56S0 18.8 0 17.93C0 8.04 8.04 0 17.93 0C18.8 0 19.5 0.7 19.5 1.56z'
+ break
+ }
+
+ parentSvg
+ .append('path')
+ .attr('opacity', '1')
+ .attr('class', order)
+ .attr('d', path)
+ .style('transform', `scale(${scale})`)
+}
+
+function addMovedOutLine(parentSvg, order, scale = 1) {
+ let path
+
+ switch (order) {
+ case 'first':
+ path =
+ 'M19.5 1.56c0 0.86-0.7 1.56-1.56 1.56c-8.16 0-14.8 6.64-14.8 14.8c0 0.86-0.7 1.56-1.56 1.56S0 18.8 0 17.93C0 8.04 8.04 0 17.93 0C18.8 0 19.5 0.7 19.5 1.56z'
+ break
+ case 'second':
+ path =
+ 'M19.5 34.44c0-.86-.7-1.56-1.56-1.56c-8.16 0-14.8-6.64-14.8-14.8c0-.86-.7-1.56-1.56-1.56S0 17.2 0 18.07C0 27.96 8.04 36 17.93 36C18.8 36 19.5 35.3 19.5 34.44z'
+ break
+ case 'third':
+ path =
+ 'M16.5 1.56c0 .86.7 1.56 1.56 1.56c8.16 0 14.8 6.64 14.8 14.8c0 .86.7 1.56 1.56 1.56c.86 0 1.56-.7 1.56-1.56C36 8.04 27.96 0 18.07 0C17.2 0 16.5.7 16.5 1.56z'
+ break
+ case 'fourth':
+ path =
+ 'M16.5 34.44c0-.86.7-1.56 1.56-1.56c8.16 0 14.8-6.64 14.8-14.8c0-.86.7-1.56 1.56-1.56c.86 0 1.56.7 1.56 1.56C36 27.96 27.96 36 18.07 36C17.2 36 16.5 35.3 16.5 34.44z'
+ break
+ }
+
+ parentSvg
+ .append('path')
+ .attr('opacity', '1')
+ .attr('class', order)
+ .attr('d', path)
+ .style('transform', `scale(${scale})`)
+}
+
function drawBlipCircle(group, blip, xValue, yValue, order) {
group
.attr('transform', `scale(1) translate(${xValue - 16}, ${yValue - 16})`)
@@ -138,6 +198,16 @@ function newBlip(blip, xValue, yValue, order, group) {
addOuterCircle(group, order, blip.scale)
}
+function movedInBlip(blip, xValue, yValue, order, group) {
+ drawBlipCircle(group, blip, xValue, yValue, order)
+ addMovedInLine(group, order, blip.scale)
+}
+
+function movedOutBlip(blip, xValue, yValue, order, group) {
+ drawBlipCircle(group, blip, xValue, yValue, order)
+ addMovedOutLine(group, order, blip.scale)
+}
+
function existingBlip(blip, xValue, yValue, order, group) {
drawBlipCircle(group, blip, xValue, yValue, order)
}
@@ -175,6 +245,10 @@ function drawBlipInCoordinates(blip, coordinates, order, quadrantGroup) {
groupBlip(blip, x, y, order, group)
} else if (blip.isNew()) {
newBlip(blip, x, y, order, group)
+ } else if (blip.hasMovedIn()) {
+ movedInBlip(blip, x, y, order, group)
+ } else if (blip.hasMovedOut()) {
+ movedOutBlip(blip, x, y, order, group)
} else {
existingBlip(blip, x, y, order, group)
}
diff --git a/src/graphing/components/quadrants.js b/src/graphing/components/quadrants.js
index 100014cac..da97fb283 100644
--- a/src/graphing/components/quadrants.js
+++ b/src/graphing/components/quadrants.js
@@ -390,7 +390,7 @@ function renderRadarQuadrants(size, svg, quadrant, rings, ringCalculator, tip) {
return quadrantGroup
}
-function renderRadarLegends(radarElement) {
+function renderRadarLegends(radarElement, hasMovements) {
const legendsContainer = radarElement.append('div').classed('radar-legends', true)
const newImage = legendsContainer
@@ -401,6 +401,14 @@ function renderRadarLegends(radarElement) {
.attr('alt', 'new blip legend icon')
.node().outerHTML
+ const movedImage = legendsContainer
+ .append('img')
+ .attr('src', '/images/moved.svg')
+ .attr('width', '37px')
+ .attr('height', '37px')
+ .attr('alt', `moved in or out blip legend icon`)
+ .node().outerHTML
+
const existingImage = legendsContainer
.append('img')
.attr('src', '/images/existing.svg')
@@ -409,7 +417,19 @@ function renderRadarLegends(radarElement) {
.attr('alt', 'existing blip legend icon')
.node().outerHTML
- legendsContainer.html(`${newImage} New ${existingImage} Existing`)
+ const noChangeImage = legendsContainer
+ .append('img')
+ .attr('src', '/images/no-change.svg')
+ .attr('width', '37px')
+ .attr('height', '37px')
+ .attr('alt', 'no change blip legend icon')
+ .node().outerHTML
+
+ if (hasMovements) {
+ legendsContainer.html(`${newImage} New ${movedImage} Moved in/out ${noChangeImage} No change`)
+ } else {
+ legendsContainer.html(`${newImage} New ${existingImage} Existing`)
+ }
}
function renderMobileView(quadrant) {
diff --git a/src/graphing/radar.js b/src/graphing/radar.js
index e4d791525..76954834f 100644
--- a/src/graphing/radar.js
+++ b/src/graphing/radar.js
@@ -828,12 +828,26 @@ const Radar = function (size, radar) {
})
if (featureToggles.UIRefresh2022) {
- renderRadarLegends(radarElement)
+ renderRadarLegends(radarElement, hasMovementData(quadrants))
hideTooltipOnScroll(tip)
addRadarLinkInPdfView()
}
}
+ function hasMovementData(quadrants) {
+ for (var quadrantWrapper of quadrants) {
+ let quadrant = quadrantWrapper.quadrant
+
+ for (var blip of quadrant.blips()) {
+ if (blip.status() !== '') {
+ return true
+ }
+ }
+ }
+
+ return false
+ }
+
return self
}
diff --git a/src/images/moved.svg b/src/images/moved.svg
new file mode 100644
index 000000000..98e75152f
--- /dev/null
+++ b/src/images/moved.svg
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
diff --git a/src/images/no-change.svg b/src/images/no-change.svg
new file mode 100644
index 000000000..98f4d440d
--- /dev/null
+++ b/src/images/no-change.svg
@@ -0,0 +1,9 @@
+
+
+
diff --git a/src/models/blip.js b/src/models/blip.js
index 25f0ff1a5..ae5ef3930 100644
--- a/src/models/blip.js
+++ b/src/models/blip.js
@@ -1,6 +1,6 @@
const { graphConfig } = require('../graphing/config')
const IDEAL_BLIP_WIDTH = 22
-const Blip = function (name, ring, isNew, topic, description) {
+const Blip = function (name, ring, isNew, status, topic, description) {
let self, blipText, isGroup, id, groupIdInGraph
self = {}
@@ -29,9 +29,29 @@ const Blip = function (name, ring, isNew, topic, description) {
}
self.isNew = function () {
+ if (status) {
+ return status.toLowerCase() === 'new'
+ }
+
return isNew
}
+ self.hasMovedIn = function () {
+ return status.toLowerCase() === 'moved in'
+ }
+
+ self.hasMovedOut = function () {
+ return status.toLowerCase() === 'moved out'
+ }
+
+ self.hasNoChange = function () {
+ return status.toLowerCase() === 'no change'
+ }
+
+ self.status = function () {
+ return status.toLowerCase() || ''
+ }
+
self.isGroup = function () {
return isGroup
}
diff --git a/src/models/radar.js b/src/models/radar.js
index ab7b90c5e..e26aeba20 100644
--- a/src/models/radar.js
+++ b/src/models/radar.js
@@ -65,6 +65,7 @@ const Radar = function () {
setNumbers(quadrant.blips())
addingQuadrant++
}
+
self.addRings = function (allRings) {
rings = allRings
}
diff --git a/src/stylesheets/_quadrants.scss b/src/stylesheets/_quadrants.scss
index b105ba82c..2afcc513b 100644
--- a/src/stylesheets/_quadrants.scss
+++ b/src/stylesheets/_quadrants.scss
@@ -402,8 +402,8 @@
top: calc($quadrantWidth * 2 + $quadrantsGap);
}
- img:nth-child(2) {
- margin-left: 48px;
+ img:nth-child(n + 2) {
+ margin-left: 24px;
}
}
diff --git a/src/util/contentValidator.js b/src/util/contentValidator.js
index 01c598652..e0bd533d9 100644
--- a/src/util/contentValidator.js
+++ b/src/util/contentValidator.js
@@ -21,11 +21,16 @@ const ContentValidator = function (columnNames) {
}
self.verifyHeaders = function () {
- _.each(['name', 'ring', 'quadrant', 'isNew', 'description'], function (field) {
+ _.each(['name', 'ring', 'quadrant', 'description'], function (field) {
if (columnNames.indexOf(field) === -1) {
throw new MalformedDataError(ExceptionMessages.MISSING_HEADERS)
}
})
+
+ // At least one of isNew or status must be present
+ if (columnNames.indexOf('isNew') === -1 && columnNames.indexOf('status') === -1) {
+ throw new MalformedDataError(ExceptionMessages.MISSING_HEADERS)
+ }
}
return self
diff --git a/src/util/factory.js b/src/util/factory.js
index 3f6772060..5cad3b873 100644
--- a/src/util/factory.js
+++ b/src/util/factory.js
@@ -53,7 +53,14 @@ const plotRadar = function (title, blips, currentRadarName, alternativeRadars) {
quadrants[blip.quadrant] = new Quadrant(blip.quadrant[0].toUpperCase() + blip.quadrant.slice(1))
}
quadrants[blip.quadrant].add(
- new Blip(blip.name, ringMap[blip.ring], blip.isNew.toLowerCase() === 'true', blip.topic, blip.description),
+ new Blip(
+ blip.name,
+ ringMap[blip.ring],
+ blip.isNew.toLowerCase() === 'true',
+ blip.status,
+ blip.topic,
+ blip.description,
+ ),
)
})
@@ -110,6 +117,7 @@ const plotRadarGraph = function (title, blips, currentRadarName, alternativeRada
blip.name,
ringMap[ring],
blip.isNew.toLowerCase() === 'true',
+ blip.status,
blip.topic,
blip.description,
)
diff --git a/src/util/inputSanitizer.js b/src/util/inputSanitizer.js
index 974260d81..53281a924 100644
--- a/src/util/inputSanitizer.js
+++ b/src/util/inputSanitizer.js
@@ -33,6 +33,7 @@ const InputSanitizer = function () {
blip.description = sanitizeHtml(blip.description, relaxedOptions)
blip.name = sanitizeHtml(blip.name, restrictedOptions)
blip.isNew = sanitizeHtml(blip.isNew, restrictedOptions)
+ blip.status = sanitizeHtml(blip.status, restrictedOptions)
blip.ring = sanitizeHtml(blip.ring, restrictedOptions)
blip.quadrant = sanitizeHtml(blip.quadrant, restrictedOptions)
@@ -45,18 +46,21 @@ const InputSanitizer = function () {
const descriptionIndex = header.indexOf('description')
const nameIndex = header.indexOf('name')
const isNewIndex = header.indexOf('isNew')
+ const statusIndex = header.indexOf('status')
const quadrantIndex = header.indexOf('quadrant')
const ringIndex = header.indexOf('ring')
const description = descriptionIndex === -1 ? '' : blip[descriptionIndex]
const name = nameIndex === -1 ? '' : blip[nameIndex]
const isNew = isNewIndex === -1 ? '' : blip[isNewIndex]
+ const status = statusIndex === -1 ? '' : blip[statusIndex]
const ring = ringIndex === -1 ? '' : blip[ringIndex]
const quadrant = quadrantIndex === -1 ? '' : blip[quadrantIndex]
blip.description = sanitizeHtml(description, relaxedOptions)
blip.name = sanitizeHtml(name, restrictedOptions)
blip.isNew = sanitizeHtml(isNew, restrictedOptions)
+ blip.status = sanitizeHtml(status, restrictedOptions)
blip.ring = sanitizeHtml(ring, restrictedOptions)
blip.quadrant = sanitizeHtml(quadrant, restrictedOptions)
diff --git a/src/util/sheet.js b/src/util/sheet.js
index 341f5bfdb..e84212f54 100644
--- a/src/util/sheet.js
+++ b/src/util/sheet.js
@@ -66,7 +66,7 @@ const Sheet = function (sheetReference) {
const sheetNames = self.sheetResponse.result.sheets.map((s) => s.properties.title)
sheetName = !sheetName ? sheetNames[0] : sheetName
self
- .getData(sheetName + '!A1:E')
+ .getData(sheetName + '!A1:F')
.then((r) => createBlips(self.sheetResponse.result.properties.title, r.result.values, sheetNames))
.catch(handleError)
}