diff --git a/app/package-lock.json b/app/package-lock.json index 404b505..905e077 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -28,9 +28,9 @@ } }, "@types/d3": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@types/d3/-/d3-4.10.0.tgz", - "integrity": "sha512-xvibY2vIwZbCCAoU2v54fvl4Sy+Qu+10eec+unuWX+E6jKQKtSrceuCFikFpweZN78vTVAdVpeqjiuK1UCH7OQ==", + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-4.11.1.tgz", + "integrity": "sha512-CJHYsKXAAaa+ZNQS4zb+w4sNWpQJn1wavEBYQ+G7RrX0Ltb24NKv/kxlDGKzYPQ/5Z6KYZ52qcaI/ViyacQ41Q==", "requires": { "@types/d3-array": "1.2.1", "@types/d3-axis": "1.0.9", @@ -39,12 +39,12 @@ "@types/d3-collection": "1.0.5", "@types/d3-color": "1.0.5", "@types/d3-dispatch": "1.0.5", - "@types/d3-drag": "1.1.0", - "@types/d3-dsv": "1.0.30", + "@types/d3-drag": "1.2.0", + "@types/d3-dsv": "1.0.31", "@types/d3-ease": "1.0.7", - "@types/d3-force": "1.0.7", + "@types/d3-force": "1.1.0", "@types/d3-format": "1.2.1", - "@types/d3-geo": "1.7.0", + "@types/d3-geo": "1.9.3", "@types/d3-hierarchy": "1.1.0", "@types/d3-interpolate": "1.1.6", "@types/d3-path": "1.0.6", @@ -57,11 +57,11 @@ "@types/d3-selection": "1.1.0", "@types/d3-shape": "1.2.1", "@types/d3-time": "1.0.7", - "@types/d3-time-format": "2.0.5", + "@types/d3-time-format": "2.1.0", "@types/d3-timer": "1.0.6", - "@types/d3-transition": "1.1.0", + "@types/d3-transition": "1.1.1", "@types/d3-voronoi": "1.1.7", - "@types/d3-zoom": "1.5.0" + "@types/d3-zoom": "1.7.0" } }, "@types/d3-array": { @@ -106,17 +106,17 @@ "integrity": "sha1-8fkYe1OOywUVdWnY3C9w37BPG1I=" }, "@types/d3-drag": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-1.1.0.tgz", - "integrity": "sha1-kQXjXKWKoMR4PzzoMIK8skzLaWA=", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-1.2.0.tgz", + "integrity": "sha512-AePmm0sXj0Tpl0uQWvwmbAf1QR3yCy9aRhjJ9mRDDSZlHBdY0SCpUtdZC9uG9Q+pyHT/dEt1R2FT/sj+5k/bVA==", "requires": { "@types/d3-selection": "1.1.0" } }, "@types/d3-dsv": { - "version": "1.0.30", - "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-1.0.30.tgz", - "integrity": "sha1-eODd3eQoNWb0Y+UVUal6Y8Fw1ag=" + "version": "1.0.31", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-1.0.31.tgz", + "integrity": "sha512-UCAVZdwd2NkrbkF1lZu9vzTlmUENRRrPCubyhDPlG8Ye1B8Xr2PNvk/Tp8tMm6sPoWZWagri6/P9H+t7WqkGDg==" }, "@types/d3-ease": { "version": "1.0.7", @@ -124,9 +124,9 @@ "integrity": "sha1-k6MBhovp4VBh89RDQ7GrP4rLbwk=" }, "@types/d3-force": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-1.0.7.tgz", - "integrity": "sha1-jjxTNpcUPrtwJ11WhAIG6Lp4kYU=" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-1.1.0.tgz", + "integrity": "sha512-a39Uu/ltLaMpj6K0elEB1oAqhx9rlTB5X/O75uTUqyTW2CfjhPXg6hFsX1lF8oeMc29kqGJZ4g9Pf6mET25bVw==" }, "@types/d3-format": { "version": "1.2.1", @@ -134,11 +134,11 @@ "integrity": "sha512-dXhA9jzCbzu6Va8ZVUQ60nu6jqA5vhPhKGR4nY3lDYfjT05GyKEKuEhfwTaSTybWczY4nLEkzv9wLQCQd5+3cA==" }, "@types/d3-geo": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-1.7.0.tgz", - "integrity": "sha512-8omkFa7oJ2U2+QzLNaTFpYcMV1zQlJXTvNlWjM8tXtvyc6H5oXglPaFAEly9T7FrncF9Kv8YLCmxaEzMqrFJ2Q==", + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-1.9.3.tgz", + "integrity": "sha512-eW1rhr/O/IXP1DXmofIMjhyuP4OpUuaWZWVlg57KndnBLeIu667z62v49ApukjiP5j2PsesOKb/6pNpL8HCPeA==", "requires": { - "@types/geojson": "1.0.4" + "@types/geojson": "1.0.6" } }, "@types/d3-hierarchy": { @@ -184,7 +184,7 @@ "resolved": "https://registry.npmjs.org/@types/d3-request/-/d3-request-1.0.2.tgz", "integrity": "sha1-2524FU9HgWWEcGxub3Ar5m8i9L4=", "requires": { - "@types/d3-dsv": "1.0.30" + "@types/d3-dsv": "1.0.31" } }, "@types/d3-scale": { @@ -214,9 +214,9 @@ "integrity": "sha512-X5ZQYiJIM38XygNwld4gZ++Vtw2ftgo3KOfZOY4n/sCudUxclxf/3THBvuG8UqSV+EQ0ezYjT5eyvcrrmixOWA==" }, "@types/d3-time-format": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.0.5.tgz", - "integrity": "sha1-HUxbp37VNSsQx/zgYsiDOC8eFuA=" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.1.0.tgz", + "integrity": "sha512-/myT3I7EwlukNOX2xVdMzb8FRgNzRMpsZddwst9Ld/VFe6LyJyRp0s32l/V9XoUzk+Gqu56F/oGk6507+8BxrA==" }, "@types/d3-timer": { "version": "1.0.6", @@ -224,9 +224,9 @@ "integrity": "sha1-eG1OIHMa3wOvLF32yG/ilmf+Qps=" }, "@types/d3-transition": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-1.1.0.tgz", - "integrity": "sha1-dEddSo+KCUSlF9XvhhlwzDAofkA=", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-1.1.1.tgz", + "integrity": "sha512-GHTghl0YYB8gGgbyKxVLHyAp9Na0HqsX2U7M0u0lGw4IdfEaslooykweZ8fDHW13T+KZeZAuzhbmqBZVFO+6kg==", "requires": { "@types/d3-selection": "1.1.0" } @@ -237,9 +237,9 @@ "integrity": "sha512-/dHFLK5jhXTb/W4XEQcFydVk8qlIAo85G3r7+N2fkBFw190l0R1GQ8C1VPeXBb2GfSU5GbT2hjlnE7i7UY5Gvg==" }, "@types/d3-zoom": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-1.5.0.tgz", - "integrity": "sha512-P8YKbLD0uGK9FisKZYbbTY4O8F7WykM9YSQoQdoY6+LYwImSzAvuWIM6BB+uD832dBiHlj1EMAZdtBqH7GEstA==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-1.7.0.tgz", + "integrity": "sha512-eIivt2ehMUXqS0guuVzRSMr5RGhO958g9EKxIJv3Z23suPnX4VQI9k1TC/bLuwKq0IWp9a1bEEcIy+PNJv9BtA==", "requires": { "@types/d3-interpolate": "1.1.6", "@types/d3-selection": "1.1.0" @@ -251,9 +251,9 @@ "integrity": "sha1-zPY207eU/mWkGR68f/l5p47+psI=" }, "@types/geojson": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-1.0.4.tgz", - "integrity": "sha512-idP+xKlqFG1egc5M52mDat/Z0VMrwY93LCd81dzW/IjeTIYTMWuzVu+fBf19QK/mX9K7jM2UNN5nzDRgM950GA==" + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-1.0.6.tgz", + "integrity": "sha512-Xqg/lIZMrUd0VRmSRbCAewtwGZiAk3mEUDvV4op1tGl+LvyPcb/MIOSxTl9z+9+J+R4/vpjiCAT4xeKzH9ji1w==" }, "@types/history": { "version": "4.6.0", @@ -1727,23 +1727,23 @@ } }, "d3": { - "version": "4.10.2", - "resolved": "https://registry.npmjs.org/d3/-/d3-4.10.2.tgz", - "integrity": "sha512-0PxXZbD+Remq9x4wdes1gs6rYcGJKA3+e0xwbma0r4ricKOKBHUHfDWcxKQICS3ZZxhzwNYWl196pxDqcAgRpw==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-4.11.0.tgz", + "integrity": "sha512-o048nfmydnbt0ciIvCUDTq9p62rZYOXzl8cKps0XVzk+5nHgeXmAS7jU4nh+3v82pUyH7t/GFm1bJRX4oIAlPw==", "requires": { - "d3-array": "1.2.0", + "d3-array": "1.2.1", "d3-axis": "1.0.8", "d3-brush": "1.0.4", "d3-chord": "1.0.4", "d3-collection": "1.0.4", "d3-color": "1.0.3", "d3-dispatch": "1.0.3", - "d3-drag": "1.1.1", + "d3-drag": "1.2.1", "d3-dsv": "1.0.7", "d3-ease": "1.0.3", - "d3-force": "1.0.6", + "d3-force": "1.1.0", "d3-format": "1.2.0", - "d3-geo": "1.6.4", + "d3-geo": "1.8.1", "d3-hierarchy": "1.1.5", "d3-interpolate": "1.1.5", "d3-path": "1.0.5", @@ -1760,7 +1760,34 @@ "d3-timer": "1.0.7", "d3-transition": "1.1.0", "d3-voronoi": "1.1.2", - "d3-zoom": "1.5.0" + "d3-zoom": "1.6.0" + }, + "dependencies": { + "d3-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.1.tgz", + "integrity": "sha512-CyINJQ0SOUHojDdFDH4JEM0552vCR1utGyLHegJHyYH0JyCpSeTPxi4OBqHMA2jJZq4NH782LtaJWBImqI/HBw==" + }, + "d3-drag": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.1.tgz", + "integrity": "sha512-Cg8/K2rTtzxzrb0fmnYOUeZHvwa4PHzwXOLZZPwtEs2SKLLKLXeYwZKBB+DlOxUvFmarOnmt//cU4+3US2lyyQ==", + "requires": { + "d3-dispatch": "1.0.3", + "d3-selection": "1.1.0" + } + }, + "d3-force": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.1.0.tgz", + "integrity": "sha512-2HVQz3/VCQs0QeRNZTYb7GxoUCeb6bOzMp/cGcLa87awY9ZsPvXOGeZm0iaGBjXic6I1ysKwMn+g+5jSAdzwcg==", + "requires": { + "d3-collection": "1.0.4", + "d3-dispatch": "1.0.3", + "d3-quadtree": "1.0.3", + "d3-timer": "1.0.7" + } + } } }, "d3-array": { @@ -1850,9 +1877,9 @@ "integrity": "sha1-a0gLqohohdRlHcJIqPSsnaFtsHo=" }, "d3-geo": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.6.4.tgz", - "integrity": "sha1-8g4eRhyxhF9ai+Vatvh2VCp+MZk=", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.8.1.tgz", + "integrity": "sha512-PuvmWl1A1UXuaxcH55EGNhvMNAUBS0RQN2PAnxrZbDvDX56Xwkd+Yp1t1+ECkaJO3my+dnhxAyqAKMyyK+IFPQ==", "requires": { "d3-array": "1.2.0" } @@ -1970,9 +1997,9 @@ "integrity": "sha1-Fodmfo8TotFYyAwUgMWinLDYlzw=" }, "d3-zoom": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-1.5.0.tgz", - "integrity": "sha512-tc/ONeSUVuwHczjjK4jQPd0T1iZ+lfsz8TbguAAceY5qs057hp4WLglkPWValkuVjCyeGpqiA2iTm8S++NJ84w==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-1.6.0.tgz", + "integrity": "sha512-viq+6rXA9JQY1wD+gpDEdlOtCeJ6IfcsNT2aVr31VTjIAIhYlO0YurQ80yDRZeMJw5P/e6nVPhGJF9D9Ade5Og==", "requires": { "d3-dispatch": "1.0.3", "d3-drag": "1.1.1", diff --git a/app/src/application/components/d3Blocks/bubble-graph.less b/app/src/application/components/d3Blocks/bubbleGraph.less similarity index 100% rename from app/src/application/components/d3Blocks/bubble-graph.less rename to app/src/application/components/d3Blocks/bubbleGraph.less diff --git a/app/src/application/components/d3Blocks/bubble-graph.tsx b/app/src/application/components/d3Blocks/bubbleGraph.tsx similarity index 99% rename from app/src/application/components/d3Blocks/bubble-graph.tsx rename to app/src/application/components/d3Blocks/bubbleGraph.tsx index 490d23e..f915281 100644 --- a/app/src/application/components/d3Blocks/bubble-graph.tsx +++ b/app/src/application/components/d3Blocks/bubbleGraph.tsx @@ -3,7 +3,7 @@ import * as d3 from 'd3' import * as React from 'react' import * as ReactDOM from 'react-dom' -import './bubble-graph.less' +import './bubbleGraph.less' interface Props { version: any, diff --git a/app/src/application/components/d3Blocks/conceptGraph.tsx b/app/src/application/components/d3Blocks/conceptGraph.tsx index 19fbf36..6b57a22 100644 --- a/app/src/application/components/d3Blocks/conceptGraph.tsx +++ b/app/src/application/components/d3Blocks/conceptGraph.tsx @@ -63,6 +63,7 @@ export class ConceptGraph extends React.Component { domNodes: d3.Selection domLabels: d3.Selection domLinks: d3.Selection + domLabelsTSpan: any highlightedNodes: { [key: string]: number @@ -113,7 +114,10 @@ export class ConceptGraph extends React.Component { .attr('cx', (d: d3GraphNode) => Math.max(30 / (d.depth + 1), Math.min(this.width - 30 / (d.depth + 1), d.x))) .attr('cy', (d: d3GraphNode) => Math.max(30 / (d.depth + 1), Math.min(this.width - 30 / (d.depth + 1), d.y))) - this.domLabels + // this.domLabels + // .attr('x', (d: d3GraphNode) => d.x) + // .attr('y', (d: d3GraphNode) => d.y) + this.domLabelsTSpan .attr('x', (d: d3GraphNode) => d.x) .attr('y', (d: d3GraphNode) => d.y) } @@ -164,7 +168,7 @@ export class ConceptGraph extends React.Component { .links(this.links) .distance(60 + (this.cc.length > 0 ? (50 / this.cc.length ) : 0)) this.simulation.force('charge') - .strength(-200 - (this.cc.length > 0 ? (100 / this.cc.length ) : 0)) + .strength(-400 - (this.cc.length > 0 ? (100 / this.cc.length ) : 0)) d3.dragDisable(window) } @@ -217,6 +221,51 @@ export class ConceptGraph extends React.Component { ) } + wrap(texts: any) { + let width = 100 + // console.log(texts) + + // TODO: investigate why function() {} and () => {} + // don't yield the same value for `this`... + texts.each(function() { + // console.log(d3.select(this as any)) + let text = d3.select(this as any), + words = text.text().split(/\s+/).reverse(), + word, + line: any= [], + lineNumber = 0, + lineHeight = 1.1, // ems + y = text.attr('y'), + x = text.attr('x'), + dy = isNaN(parseFloat(text.attr('dy'))) ? 0.2 : parseFloat(text.attr('dy')), + tspan = text + .text(null) + .append('tspan') + .attr('x', x) + .attr('y', y) + .attr('dy', dy + 'em') + // console.log([y, x, dy]) + + while (word = words.pop()) { + line.push(word) + tspan.text(line.join(' ')) + var node: any = tspan.node() + var hasGreaterWidth = node.getComputedTextLength() > width + if (hasGreaterWidth) { + line.pop() + tspan.text(line.join(' ')) + line = [word] + tspan = text + .append('tspan') + .attr('x', x) + .attr('y', y) + .attr('dy', ++lineNumber * lineHeight + dy + 'em') + .text(word) + } + } + }) + } + // Same use as renderNodes, this time for labels... renderLabels() { this.domLabels = this.domContainer @@ -233,6 +282,10 @@ export class ConceptGraph extends React.Component { .attr('x', (d: d3GraphNode) => d.x) .attr('y', (d: d3GraphNode) => d.y) .text((d: d3GraphNode) => d.name) + .call(this.wrap) + + this.domLabelsTSpan = this.domContainer + .selectAll('tspan') } // ... and this time for links. diff --git a/app/src/application/components/d3Blocks/conceptHierarchy.tsx b/app/src/application/components/d3Blocks/conceptHierarchy.tsx index 0082337..805b120 100644 --- a/app/src/application/components/d3Blocks/conceptHierarchy.tsx +++ b/app/src/application/components/d3Blocks/conceptHierarchy.tsx @@ -137,7 +137,7 @@ export class ConceptHierarchy extends React.Component { } } tooltipDimensions = { - w: 120, + w: 200, h: 15, p: { t: 10, @@ -294,7 +294,7 @@ export class ConceptHierarchy extends React.Component { that.domSvg.select('#tooltip_content') .text(d.data.name) - .call(_.partial(wrap.wrap, 120)) + .call(_.partial(wrap.wrap, that.tooltipDimensions.w - 20)) that.domSvg.select('#tooltip') .attr('transform', () => { diff --git a/app/src/application/components/d3Blocks/sunburst.less b/app/src/application/components/d3Blocks/sunburst.less index 35dfcc1..604f32d 100644 --- a/app/src/application/components/d3Blocks/sunburst.less +++ b/app/src/application/components/d3Blocks/sunburst.less @@ -1,15 +1,65 @@ @import '~@blueprintjs/core/dist/variables.less'; +#main { + .flex-box { + align-content: center; + } +} + +#toolbox { + display: flex; + align-items: center; + + > * { + margin: 5px; + } + + .searchBar { + flex-basis: 40%; + } +} + path { stroke: @dark-gray4; stroke-width: 2px; stroke-opacity: 1; } +#svgDiv { + flex-grow: 2; + + #chart { + text-align: center; + } +} + #breadcrumbs { - height: 18px; + flex-grow: 1; + flex-basis: 20%; + align-items: center; + + min-width: 100px; font-size: 15px; + margin: 10px; + display: flex; + + #breadcrumb-list { + padding: 5px; + background-color: @dark-gray3; + } + + li { + display: flex; + align-items: center; + margin: 3px; + + a { + background-color: @dark-gray5; + padding: 2px 5px 2px 5px; + } + } } + #info { text-anchor: middle; @@ -27,3 +77,35 @@ path { font-size: 13px; } } + +#values { + text-align: center; + margin: 6px 0 16px 0; + + #total, #relative, #absoluteValue { + display: inline-block; + padding: 10px; + font-size: 15px; + line-height: 15px; + height: 35px; + margin: 0; + } + + #total { + background-color: @dark-gray5; + } + + #relative { + background-color: @blue1; + } + + #absoluteValue { + background-color: @gray1; + } + + #percentages { + width: 100%; + height: 35px; + padding: 3px; + } +} diff --git a/app/src/application/components/d3Blocks/sunburst.tsx b/app/src/application/components/d3Blocks/sunburst.tsx index c438ec7..c45059e 100644 --- a/app/src/application/components/d3Blocks/sunburst.tsx +++ b/app/src/application/components/d3Blocks/sunburst.tsx @@ -1,7 +1,11 @@ +import { + Switch, +} from '@blueprintjs/core' import * as _ from 'lodash' import * as d3 from 'd3' import * as React from 'react' import * as ReactDOM from 'react-dom' +import * as slug from 'slug' import './sunburst.less' @@ -20,95 +24,220 @@ interface Props { left: number, }, colors: any, + data: any, } interface State { - + displayValue: boolean, + displaySearchResults: boolean, + searchedString: string, + selectedHierarchy: any, } export class Sunburst extends React.Component { + d3Hierarchy: any + selectedHierarchy: any + nodes: any + filteredNodes: any + // D3 refs: any domSvg: any domContainer: any + size: number + height: number + width: number radius: any + xScale: any + xTargetScale: any + yScale: any partition: any arc: any b: any totalSize: any + relativeSize: any path: any - initAttributes() { - this.totalSize = 0 - this.arc = d3.arc() - .startAngle((d: any) => d.x0) - .endAngle((d: any) => d.x1) - .innerRadius((d: any) => Math.sqrt(d.y0)) - .outerRadius((d: any) => Math.sqrt(d.y1)) - } - updateAttributes() { + let defaultData = ` + orion|sunburst|redo;120 + orion|sunburst|understand;50 + orion|sunburst|typing;10 + orion|fun;40 + mva|homework;30 + ` + + let data: string = this.props.data ? this.props.data : defaultData + + let csv = d3.dsvFormat(';').parseRows(data) + let hierarchy = this.buildHierarchy(csv) + + this.d3Hierarchy = d3.hierarchy(hierarchy) + .sum((d: any) => d.size) + .sort((a: any, b: any) => b.value - a.value) + + let i = 0 + let cIndex = 0 + this.d3Hierarchy.each((node: any) => { + if (node.depth == 1) { + node.data.cIndex = cIndex + cIndex++ + } + node.data.index = i + i++ + }) + + this.totalSize = this.d3Hierarchy.value + this.selectedHierarchy = this.d3Hierarchy + this.nodes = this.d3Hierarchy.descendants() + + this.height = this.props.dimensions.height + this.width = this.props.dimensions.width + this.size = Math.min(this.height, this.width) + this.domSvg - .attr('width', this.props.dimensions.width) - .attr('height', this.props.dimensions.height) + .attr('width', this.size) + .attr('height', this.size) this.domContainer - .attr('transform', 'translate(' + this.props.dimensions.width / 2 + ',' + this.props.dimensions.height / 2 + ')') + .attr('transform', 'translate(' + this.size / 2 + ',' + this.size / 2 + ')') this.domSvg.select('#info') - .attr('transform', 'translate(' + this.props.dimensions.width / 2 + ',' + this.props.dimensions.height / 2 + ')') + .attr('transform', 'translate(' + this.size / 2 + ',' + this.size / 2 + ')') - this.radius = Math.min(this.props.dimensions.width, this.props.dimensions.height) / 2 + this.radius = Math.min(this.size, this.size) / 2 this.partition = d3.partition() - .size([2 * Math.PI, this.radius * this.radius]) - // .padding(0.1) + .size([1, 1]) + + this.xScale = d3.scaleLinear() + .range([0, 2 * Math.PI]) + + this.xTargetScale = d3.scaleLinear() + .range([0, 1]) + + this.yScale = d3.scaleSqrt() + .range([0, this.radius]) + + this.arc = d3.arc() + .startAngle((d: any) => Math.max(0, Math.min(2 * Math.PI, this.xScale(d.x0)))) + .endAngle((d: any) => Math.max(0, Math.min(2 * Math.PI, this.xScale(d.x1)))) + .innerRadius((d: any) => Math.max(0, this.yScale(d.y0))) + .outerRadius((d: any) => Math.max(0, this.yScale(d.y1))) + } - drawSunburst(hierarchy: any) { - let root = d3.hierarchy(hierarchy) - .sum((d: any) => d.size) - .sort((a: any, b: any) => b.value - a.value) + updateFilteredNodes() { + let descendants = this.selectedHierarchy.descendants() + let ancestors = this.selectedHierarchy.ancestors() - let nodes = this.partition(root) + this.filteredNodes = this.partition(this.d3Hierarchy) .descendants() - .filter((d: any) => (d.x1 - d.x0 > 0.005)) + .filter((d: any) => { + return (ancestors.indexOf(d) != -1) || (descendants.indexOf(d) != -1 && (this.xTargetScale(d.x1) - this.xTargetScale(d.x0) > 0.005)) + }) + // For PLF2017, filtering allows to go from 3000 nodes to 300. + } + drawSunburst() { this.path = this.domContainer - .data([hierarchy]) .selectAll('path') - .data(nodes) + .data(this.filteredNodes, (d: any) => d.data.index) + + this.path.exit().remove() this.path .enter() .append('path') - .attr('display', (d: any) => d.depth ? null : 'none') + // .attr('display', (d: any) => d.depth ? null : 'none') + .on('mouseover', this.handleMouseOver.bind(this)) + .on('click', (d: any) => this.handleClick(d.data.index)) + .merge(this.path) .attr('d', this.arc) .attr('fill-rule', 'evenodd') - .style('fill', (d: any, index: number) => this.props.colors[index % this.props.colors.length]) - .merge(this.path) + .style('fill', (d: any) => { + if (d.depth != 0) { + return this.props.colors[d.ancestors().reverse()[1].data.cIndex % this.props.colors.length] + } else { + return '#5C7080' + } + }) .style('opacity', 1) - .on('mouseover', this.handleMouseOver.bind(this)) d3.select('#container').on('mouseleave', this.handleMouseleave.bind(this)) - this.totalSize = root.value + this.relativeSize = this.selectedHierarchy.value + } + + handleClick(nodeIndex: string) { + let selectedHierarchy = null + this.d3Hierarchy.each((node: any) => { + if (node.data.index == nodeIndex) { + selectedHierarchy = node + } + }) + + let selectedNode: any = null + this.nodes.forEach((node: any) => { + if (node.data.index == nodeIndex) { + selectedNode = node + } + }) + + this.xTargetScale = d3.scaleLinear() + .domain([selectedNode.x0, selectedNode.x1]) + + this.selectedHierarchy = selectedHierarchy + + this.setState({ + displaySearchResults: false, + selectedHierarchy: selectedHierarchy, + }, () => { + this.domContainer + .transition() + .duration(500) + .tween('scale', () => { + let xdomain = d3.interpolate(this.xScale.domain(), [selectedNode.x0, selectedNode.x1]) + let ydomain = d3.interpolate(this.yScale.domain(), [selectedNode.y0, 1]) + let yrange = d3.interpolate(this.yScale.range(), [selectedNode.y0 ? 30 : 0, this.radius]) + + return ((t: any) => { + this.xScale.domain(xdomain(t)) + this.yScale.domain(ydomain(t)).range(yrange(t)) + }) + }) + .selectAll('path') + .attrTween('d', (d: any) => (() => this.arc(d))) + }) + } + + + numberWithCommas = (x: number) => { + return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' ') } handleMouseOver(d: any) { - let percentage = (100 * d.value / this.totalSize).toPrecision(3) as any - let percentageString = percentage + '%' - if (percentage < 0.1) { - percentageString = '< 0.1%' + let totalPercentage = (100 * d.value / this.totalSize).toPrecision(3) as any + let totalPercentageString = totalPercentage + '%' + let relativePercentage = (100 * d.value / this.relativeSize).toPrecision(3) as any + let relativePercentageString = relativePercentage + '%' + + if (relativePercentage < 0.1) { + relativePercentageString = '< 0.1%' } - d3.select('#header') - .text(percentageString) + d3.select('#total') + .text(totalPercentageString) + + d3.select('#relative') + .text(relativePercentageString) + + d3.select('#absoluteValue') + .text(this.numberWithCommas(d.value)) - d3.select('#info') + d3.select('#values') .style('visibility', '') let sequenceArray = d.ancestors().reverse() - sequenceArray.shift() - this.updateBreadcrumbs(sequenceArray, percentageString) + this.updateBreadcrumbs(sequenceArray) d3.selectAll('path') .style('opacity', 0.3) @@ -122,22 +251,21 @@ export class Sunburst extends React.Component { d3.select('#main #breadcrumbs') .style('visibility', 'hidden') + d3.select('#values') + .style('visibility', 'hidden') + d3.selectAll('path') .transition() .duration(150) .style('opacity', 1) - - d3.select('#info') - .style('visibility', 'hidden') } - updateBreadcrumbs(nodeArray: any, percentageString: any) { + updateBreadcrumbs(nodeArray: any) { d3.select('#main #breadcrumbs').style('visibility', '') let breads = d3.select('#main #breadcrumbs #breadcrumb-list') .selectAll('li') - // .data(nodeArray, (d: any) => d.data.name + d.depth) - .data(nodeArray) + .data(nodeArray, (d: any) => d.data.name + d.depth) breads.exit().remove() breads.enter() @@ -147,20 +275,47 @@ export class Sunburst extends React.Component { .html((d: any) => d.data.name) } + // REACT LIFECYCLE + constructor(props: Props) { + super(props) + this.state = { + displayValue: true, + displaySearchResults: false, + searchedString: '', + selectedHierarchy: null, + } + } + + componentDidMount() { + this.domSvg = d3.select(this.refs.container) + this.domContainer = d3.select('#container') + } + + shouldComponentUpdate(nextProps: Props, nextState: State) { + if (this.state == nextState) { + if (nextProps.version === this.props.version) { + if (nextProps.data === this.props.data) { + return false + } + } + } + return true + } + buildHierarchy(csv: any) { let root = { - 'name': 'root', - 'children': [] as any + 'name': 'PLF', + 'children': [] as any, } for (let i = 0; i < csv.length; i++) { let sequence = csv[i][0] - let size = +csv[i][1] + let size = (csv[i].length > 1 && this.state.displayValue) ? (+csv[i][1]) : 1 if (isNaN(size)) { continue } - let parts = sequence.split('-') + let parts = sequence.split('|') let currentNode = root for (let j = 0; j < parts.length; j++) { let children = currentNode['children'] @@ -178,7 +333,7 @@ export class Sunburst extends React.Component { if (!foundChild) { childNode = { 'name': nodeName, - 'children': [] + 'children': [], } children.push(childNode) } @@ -186,7 +341,7 @@ export class Sunburst extends React.Component { } else { childNode = { 'name': nodeName, - 'size': size + 'size': size, } children.push(childNode) } @@ -195,66 +350,111 @@ export class Sunburst extends React.Component { return root } + componentDidUpdate() { + // If object is rendered for the first time since props.data + // was updated: + if(!this.state.selectedHierarchy) { + this.updateAttributes() + } - // REACT LIFECYCLE - constructor(props: Props) { - super(props) + this.updateFilteredNodes() + this.drawSunburst() } - componentDidMount() { - this.domSvg = d3.select(this.refs.container) - this.domContainer = d3.select('#container') - this.initAttributes() - this.updateAttributes() + changeDisplayValue() { + this.setState((prevState: any, props: any) => { + return { + displayValue: !prevState.displayValue, + } + }) } - shouldComponentUpdate(nextProps: Props, nextState: State) { - if (nextProps.version === this.props.version) { - return false - } - return true + searchInputFocused() { + this.setState({ + displaySearchResults: true, + }) } - componentDidUpdate() { - this.updateAttributes() - let text = ` - orion-sunburst-redo,120 - orion-sunburst-understand,50 - orion-sunburst-typing,10 - orion-fun,40 - mva-homework,30 - ` - - let csv = d3.csvParseRows(text) - let hierarchy = this.buildHierarchy(csv) - - this.drawSunburst(hierarchy) + searchInputUpdated(event: any) { + this.setState({ + searchedString: event.target.value, + }) } render() { let {width, height} = this.props.dimensions + let selectedNodes: any = [] + + if (this.state.searchedString != '') { + this.d3Hierarchy.each((node: any) => { + if (slug(node.data.name, {lower: true}).indexOf(this.state.searchedString) != -1) { + selectedNodes.push(node) + } + }) + } + + let selectedResultsLi = _.map(selectedNodes, (node: any) => { + return
  • { + this.handleClick.bind(this, node.data.index)() + }} + > +
      + {_.map(node.ancestors().reverse(), (ancestor: any, index: number) => +
    • + {ancestor.data.name} +
    • + )} +
    +
  • + }) return ( -
    -
    -