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) }