Skip to content

Commit

Permalink
Merge pull request #260 from RobokopU24/feature/color-legend
Browse files Browse the repository at this point in the history
Feature/color legend
  • Loading branch information
Woozl authored Jun 28, 2023
2 parents dfec2df + bf2e75b commit 5f8e265
Show file tree
Hide file tree
Showing 16 changed files with 97 additions and 75 deletions.
2 changes: 1 addition & 1 deletion Dockerfile.prod
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ USER nru
# Copy the code into the container
COPY . .

ENV BASE_URL /question-builder/
ENV BASE_URL /question-builder

# Build the code and save a production ready copy
CMD npm run prod
2 changes: 1 addition & 1 deletion src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export default function App() {

return (
<div id="pageContainer">
<BrowserRouter basename="/question-builder">
<BrowserRouter basename={process.env.BASE_URL}>
<Auth0Provider
domain="qgraph.us.auth0.com"
clientId="sgJrK1gGAbzrXwUp0WG7jAV0ivCIF6jr"
Expand Down
2 changes: 1 addition & 1 deletion src/pages/answer/fullKg/KgFull.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export default function KgFull({ message }) {
if (d.categories && Array.isArray(d.categories)) {
d.categories = kgUtils.getRankedCategories(hierarchies, d.categories);
}
const color = colorMap(d.categories);
const color = colorMap(d.categories)[1];
context.strokeStyle = color;
context.fillStyle = color;
context.fill();
Expand Down
87 changes: 66 additions & 21 deletions src/pages/answer/kgBubble/KgBubble.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
/* eslint-disable indent, no-use-before-define, func-names, no-return-assign */
import React, {
useState, useEffect, useRef, useContext,
useState, useEffect, useRef, useContext, useMemo,
} from 'react';
import * as d3 from 'd3';
import Paper from '@material-ui/core/Paper';
import Box from '@material-ui/core/Box';
import Slider from '@material-ui/core/Slider';

import { List, ListItem, ListItemIcon } from '@material-ui/core';
import { Brightness1 as Circle } from '@material-ui/icons';
import stringUtils from '~/utils/strings';
import BiolinkContext from '~/context/biolink';
import kgUtils from '~/utils/knowledgeGraph';
import dragUtils from '~/utils/d3/drag';
Expand Down Expand Up @@ -34,6 +37,26 @@ export default function KgBubble({
const [numTrimmedNodes, setNumTrimmedNodes] = useState(Math.min(nodes.length, defaultTrimNum));
const debouncedTrimmedNodes = useDebounce(numTrimmedNodes, 500);

const trimmedNodes = useMemo(() => nodes.slice(0, debouncedTrimmedNodes), [debouncedTrimmedNodes, nodes]);

// computes an array of [category, color], without duplicates, sorted by
// the number of occurences of the category in the `nodes` list
const categoryColorList = useMemo(() => {
const map = new Map();
trimmedNodes.forEach((node) => {
const [category, color] = colorMap(node.categories);
if (!map.has(category)) {
map.set(category, { color, occurrences: 1 });
} else {
map.get(category).occurrences += 1;
}
});

return Array.from(map)
.sort((a, b) => a.occurrences - b.occurrences)
.map(([category, { color }]) => [category, color]);
}, [trimmedNodes, colorMap]);

/**
* Initialize the svg size
*/
Expand All @@ -59,7 +82,6 @@ export default function KgBubble({
// clear the graph
svg.selectAll('*').remove();
const getNodeRadius = kgUtils.getNodeRadius(width, height, numQgNodes, numResults);
const trimmedNodes = nodes.slice(0, debouncedTrimmedNodes);
const converted_nodes = trimmedNodes.map((d) => ({ ...d, x: Math.random() * width, y: Math.random() * height }));
const simulation = d3.forceSimulation(converted_nodes)
.force('x', d3.forceX(width / 2).strength(0.02)) // pull all nodes horizontally towards middle of box
Expand Down Expand Up @@ -88,7 +110,7 @@ export default function KgBubble({
.call(dragUtils.dragNode(simulation))
.call((n) => n.append('circle')
.attr('r', (d) => getNodeRadius(d.count))
.attr('fill', (d) => colorMap(d.categories))
.attr('fill', (d) => colorMap(d.categories)[1])
.call((nCircle) => nCircle.append('title')
.text((d) => d.name)))
.call((n) => n.append('text')
Expand Down Expand Up @@ -138,24 +160,47 @@ export default function KgBubble({
return (
<>
{nodes.length > 0 && (
<Paper id="kgBubbleContainer" elevation={3}>
<h5 className="cardLabel">Knowledge Graph Bubble</h5>
{nodes.length > defaultTrimNum && (
<Box width={300} id="nodeNumSlider">
<Slider
value={numTrimmedNodes}
valueLabelDisplay="auto"
min={2}
max={nodes.length}
onChange={(e, v) => setNumTrimmedNodes(v)}
/>
</Box>
)}
{drawing && (
<Loading positionStatic message="Redrawing knowledge graph..." />
)}
<svg ref={svgRef} />
</Paper>
<div style={{
display: 'flex',
width: '100%',
gap: '10px',
margin: '10px',
}}
>
<Paper id="kgBubbleContainer" elevation={3}>
<h5 className="cardLabel">Knowledge Graph Bubble</h5>
{nodes.length > defaultTrimNum && (
<Box width={300} id="nodeNumSlider">
<Slider
value={numTrimmedNodes}
valueLabelDisplay="auto"
min={2}
max={nodes.length}
onChange={(e, v) => setNumTrimmedNodes(v)}
/>
</Box>
)}
{drawing && (
<Loading positionStatic message="Redrawing knowledge graph..." />
)}
<svg ref={svgRef} />
</Paper>
<Paper id="legendContainer" elevation={3}>
<h5 className="legendHeader">Legend</h5>
<List style={{ paddingTop: '0.5rem' }}>
{
categoryColorList.map(([category, color], i) => (
<ListItem key={i}>
<ListItemIcon style={{ minWidth: '32px' }}>
<Circle style={{ color }} />
</ListItemIcon>
{stringUtils.displayCategory(category)}
</ListItem>
))
}
</List>
</Paper>
</div>
)}
</>
);
Expand Down
12 changes: 10 additions & 2 deletions src/pages/answer/kgBubble/kgBubble.css
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
#kgBubbleContainer {
width: 100%;
flex: 1 0 0;
height: 30rem;
margin: 10px;
position: relative;
overflow: hidden;
}
#legendContainer {
height: 30rem;
overflow-x: auto;
}
.legendHeader {
padding: 5px 0px 0px 10px;
margin: 10px 0px 0px 0px;
}
#nodeNumSlider {
position: absolute;
Expand Down
2 changes: 1 addition & 1 deletion src/pages/answer/queryGraph/QueryGraph.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export default function QueryGraph({ query_graph }) {
.call(dragUtils.dragNode(simulation))
.call((n) => n.append('circle')
.attr('r', nodeRadius)
.attr('fill', (d) => colorMap(d.categories))
.attr('fill', (d) => colorMap(d.categories)[1])
.call((nCircle) => nCircle.append('title')
.text((d) => d.name)))
.call((n) => n.append('text')
Expand Down
2 changes: 1 addition & 1 deletion src/pages/answer/resultsTable/ResultExplorer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ export default function ResultExplorer({ answerStore }) {
.call(dragUtils.dragNode(simulation.current))
.call((n) => n.append('circle')
.attr('r', nodeRadius)
.attr('fill', (d) => colorMap(d.categories))
.attr('fill', (d) => colorMap(d.categories)[1])
.call((nCircle) => nCircle.append('title')
.text((d) => d.name)))
.call((n) => n.append('text')
Expand Down
6 changes: 3 additions & 3 deletions src/utils/colors.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const conceptColorMap = {
export default function getNodeCategoryColorMap(hierarchies) {
return (categories) => {
if (!categories || !Object.keys(hierarchies).length) {
return undefinedColor;
return [null, undefinedColor];
}
if (!Array.isArray(categories)) {
categories = [categories]; // eslint-disable-line
Expand All @@ -60,10 +60,10 @@ export default function getNodeCategoryColorMap(hierarchies) {
}
});
if (category !== undefined) {
return conceptColorMap[category];
return [category, conceptColorMap[category]];
}

// only if we have no predefined color in the hierarchy
return undefinedColor;
return [null, undefinedColor];
};
}
4 changes: 2 additions & 2 deletions src/utils/d3/nodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function enter(node, args) {
.call((nodeCircle) => nodeCircle.append('circle')
.attr('class', (d) => `nodeCircle node-${d.id}`)
.attr('r', nodeRadius)
.attr('fill', (d) => colorMap(d.categories))
.attr('fill', (d) => colorMap(d.categories)[1])
.style('cursor', 'pointer')
.call((n) => n.append('title')
.text((d) => {
Expand Down Expand Up @@ -120,7 +120,7 @@ function update(node, args) {
const { colorMap } = args;
return node
.call((n) => n.select('.nodeCircle')
.attr('fill', (d) => colorMap(d.categories)))
.attr('fill', (d) => colorMap(d.categories)[1]))
.style('filter', (d) => (d.is_set ? 'url(#setShadow)' : ''))
.call((nodeCircle) => nodeCircle.select('title')
.text((d) => {
Expand Down
2 changes: 1 addition & 1 deletion src/utils/results.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ function makeTableHeaders(message, colorMap) {
const sortedNodes = sortNodes(query_graph, startingNode);
const headerColumns = sortedNodes.map((id) => {
const qgNode = query_graph.nodes[id];
const backgroundColor = colorMap(qgNode.categories);
const backgroundColor = colorMap(qgNode.categories)[1];
const nodeIdLabel = queryGraphUtils.getTableHeaderLabel(qgNode);
const headerText = qgNode.name || nodeIdLabel || stringUtils.displayCategory(qgNode.categories) || 'Something';
const width = getColumnWidth(results, id, knowledge_graph.nodes, headerText);
Expand Down
8 changes: 4 additions & 4 deletions tests/common/mocks/handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import biolink from '&/biolink_model.json';
import test_message from '&/test_message.json';

const handlers = [
rest.get(`${process.env.BASE_URL || ''}/api/biolink`, (req, res, ctx) => res(
rest.get('/api/biolink', (req, res, ctx) => res(
ctx.json(biolink),
)),
rest.post(`${process.env.BASE_URL || ''}/api/node_norm`, (req, res, ctx) => {
rest.post('/api/node_norm', (req, res, ctx) => {
const curie = req.body.curies[0];
return res(
ctx.json({
Expand All @@ -21,15 +21,15 @@ const handlers = [
}),
);
}),
rest.post(`${process.env.BASE_URL || ''}/api/name_resolver`, (req, res, ctx) => {
rest.post('/api/name_resolver', (req, res, ctx) => {
const curie = req.url.searchParams.get('string');
return res(
ctx.json({
[curie]: {},
}),
);
}),
rest.post(`${process.env.BASE_URL || ''}/api/quick_answer`, (req, res, ctx) => res(
rest.post('/api/quick_answer', (req, res, ctx) => res(
ctx.json(test_message),
)),
];
Expand Down
4 changes: 2 additions & 2 deletions tests/integration/ask_question.test.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from 'react';
import axios from 'axios';
import userEvent from '@testing-library/user-event';
import {
render, waitFor, screen,
} from '&/test_utils';
import { api } from '~/API/baseUrlProxy';

import App from '~/App';

Expand Down Expand Up @@ -34,7 +34,7 @@ describe('Full question workflow', () => {
jest.clearAllMocks();
});
it('successfully asks a question', async () => {
const spyPost = jest.spyOn(axios, 'post');
const spyPost = jest.spyOn(api, 'post');
render(<App />);

// submit question
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/app.test.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import axios from 'axios';
import { rest } from 'msw';
import '@testing-library/jest-dom';
import { api } from '~/API/baseUrlProxy';
import {
render, screen, waitFor,
} from '&/test_utils';
Expand All @@ -21,7 +21,7 @@ describe('<App />', () => {
jest.clearAllMocks();
});
it('loads the Robokop homepage', async () => {
const spy = jest.spyOn(axios, 'get');
const spy = jest.spyOn(api, 'get');
server.use(
rest.get('/api/biolink', (req, res, ctx) => res(
ctx.status(404),
Expand Down
31 changes: 0 additions & 31 deletions tests/unit/question_page.test.jsx

This file was deleted.

2 changes: 1 addition & 1 deletion tests/unit/utils/resultsTable.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe('Results Table', () => {
expect(sortedNodes).toStrictEqual(['3', '1', '2', '4']);
});
it('makes table headers correctly', () => {
const tableHeaders = resultsUtils.makeTableHeaders(test_message.message, () => {});
const tableHeaders = resultsUtils.makeTableHeaders(test_message.message, () => ['', '']);
expect(tableHeaders.length).toBe(2);
const [header1, header2] = tableHeaders;
expect(header1.id).toBe('n1');
Expand Down
2 changes: 1 addition & 1 deletion webpack.prod.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ module.exports = merge(common, {
devtool: 'source-map',
mode: 'production',
output: {
publicPath: '/question-builder/',
publicPath: process.env.BASE_URL || '/',
},
});

0 comments on commit 5f8e265

Please sign in to comment.