UNPKG

@rws-framework/client

Version:

This package provides the core client-side framework for Realtime Web Suit (RWS), enabling modular, asynchronous web components, state management, and integration with backend services. It is located in `.dev/client`.

1,315 lines (1,137 loc) 54 kB
let changeCouplingMapForDateRange = {} function clickDateRangePickerCancel() { console.log("reset date range picker") if (includeGitMetrics) { initDateRangeUI() dateRangePickerFrom = commit_first_date dateRangePickerTo = commit_last_date initGitMetricsForDateRange() showToastDateRangeUpdate() } } function initGitMetricsForDateRange() { gitMetricsIndexFrom = commit_dates.indexOf( dateRangePickerFrom ); gitMetricsIndexTo = commit_dates.lastIndexOf( dateRangePickerTo ); // console.log("found first index: " + gitMetricsIndexFrom + " for `from` " + dateRangePickerFrom) // console.log("found last index: " + gitMetricsIndexTo + " for `to` " + dateRangePickerTo) changeCouplingMapForDateRange = calculateCouplingForDateRange() // console.log(changeCouplingMapForDateRange) // console.log(changeCouplingMapForDateRange) addGitMetricToFileNodes() } function nodeNamesHaveChangeCoupling(sourceName, targetName) { for (const [key, value] of Object.entries(changeCouplingMapForDateRange)) { if (sourceName.includes( key ) ) { for (const k of changeCouplingMapForDateRange[key]) { if (targetName.includes(k)) { return true } } } } return false } function calculateCouplingForDateRange() { let totalChangeCouplingDict = {} for (let i = gitMetricsIndexFrom; i < gitMetricsIndexTo; i++) { let couplingLinks = commit_metrics[i].links if (couplingLinks.length > 0) { for (nextChangeCouplingDict of couplingLinks) { // source -> target if ( !(nextChangeCouplingDict.source in totalChangeCouplingDict) ) { totalChangeCouplingDict[nextChangeCouplingDict.source] = new Set(); totalChangeCouplingDict[nextChangeCouplingDict.source].add(nextChangeCouplingDict.target) } else { totalChangeCouplingDict[nextChangeCouplingDict.source].add(nextChangeCouplingDict.target) } // target -> source if ( !(nextChangeCouplingDict.target in totalChangeCouplingDict) ) { totalChangeCouplingDict[nextChangeCouplingDict.target] = new Set(); totalChangeCouplingDict[nextChangeCouplingDict.target].add(nextChangeCouplingDict.source) } else { totalChangeCouplingDict[nextChangeCouplingDict.target].add(nextChangeCouplingDict.source) } } } } return totalChangeCouplingDict } function calculateFileChurnForDateRange() { let totalFileChurnDict = {} for (let i = gitMetricsIndexFrom; i < gitMetricsIndexTo; i++) { let nextChurnDict = commit_metrics[i].churn totalFileChurnDict = mergeDicts(totalFileChurnDict, nextChurnDict) } return totalFileChurnDict } function calculateSlocForDateRange() { let totalSlocDict = {} for (let i = gitMetricsIndexFrom; i < gitMetricsIndexTo; i++) { let nextSlocDict = commit_metrics[i].sloc totalSlocDict = mergeDictsToMostCurrentValues(totalSlocDict, nextSlocDict) } return totalSlocDict } function calculateWhiteSpaceComplexityForDateRange() { let totalWhiteSpaceComplexityDict = {} for (let i = gitMetricsIndexFrom; i < gitMetricsIndexTo; i++) { let nextWhiteSpaceComplexityDict = commit_metrics[i].ws_complexity totalWhiteSpaceComplexityDict = mergeDictsToMostCurrentValues(totalWhiteSpaceComplexityDict, nextWhiteSpaceComplexityDict) } return totalWhiteSpaceComplexityDict } function calculateAuthorsForDateRange() { let totalFileAuthorsDict = {} for (let i = gitMetricsIndexFrom; i < gitMetricsIndexTo; i++) { let nextFileAuthorsDict = commit_metrics[i].files_author_map totalFileAuthorsDict = mergeDicts(totalFileAuthorsDict, nextFileAuthorsDict) } return totalFileAuthorsDict } function addGitMetricToFileNodes() { if (currentGraphType.includes('file_result_dependency_graph')) { let fileChurnMap = calculateFileChurnForDateRange() let whiteSpaceComplexityMap = calculateWhiteSpaceComplexityForDateRange() let slocMap = calculateSlocForDateRange() let authorsMap = calculateAuthorsForDateRange() // console.log(authorsMap) // console.log(whiteSpaceComplexityMap) // console.log("fileResultPrefix: " + fileResultPrefix) currentGraph.nodes.forEach(function(node, i) { // housekeeping git code churn delete node['metric_git_code_churn'] if (node.hasOwnProperty('metrics')) { delete node.metrics['metric_git_code_churn'] } // housekeeping git ws complexity delete node['metric_git_ws_complexity'] if (node.hasOwnProperty('metrics')) { delete node.metrics['metric_git_ws_complexity'] } // housekeeping git number of file authors delete node['metric_git_number_authors'] if (node.hasOwnProperty('metrics')) { delete node.metrics['metric_git_number_authors'] } delete node['metric_git_main_contrib'] if (node.hasOwnProperty('metrics')) { delete node.metrics['metric_git_main_contrib'] } // housekeeping file contributors delete node['metric_git_contributors'] if (node.hasOwnProperty('metrics')) { delete node.metrics['metric_git_contributors'] } // housekeeping git sloc delete node['metric_git_sloc'] if (node.hasOwnProperty('metrics')) { delete node.metrics['metric_git_sloc'] } if (!node.hasOwnProperty('metrics')) { node.metrics = {} } let nodeFileName = node.id.split("/").pop(); let nodeSearchPath = "" if (fileResultPrefix === "") { nodeSearchPath = node.id } else { nodeSearchPath = fileResultPrefix + "/" + node.id } // add git code churn for (const [key, value] of Object.entries(fileChurnMap)) { if (nodeSearchPath.includes(key)) { node['metric_git_code_churn'] = value node.metrics['metric_git_code_churn'] = value } } // add git whitespace complexity for (const [key, value] of Object.entries(whiteSpaceComplexityMap)) { if (nodeSearchPath.includes(key)) { node['metric_git_ws_complexity'] = value node.metrics['metric_git_ws_complexity'] = value } } // add git sloc for (const [key, value] of Object.entries(slocMap)) { if (nodeSearchPath.includes(key)) { node['metric_git_sloc'] = value node.metrics['metric_git_sloc'] = value } } // add git number authors for (const [key, value] of Object.entries(authorsMap)) { if (nodeSearchPath.includes(key)) { node['metric_git_contributors'] = value node.metrics['metric_git_contributors'] = value } } // add all git contributors to file for (const [key, value] of Object.entries(authorsMap)) { if (nodeSearchPath.includes(key)) { node['metric_git_contributors'] = Object.keys(value) node.metrics['metric_git_contributors'] = Object.keys(value) node['metric_git_number_authors'] = Object.keys(value).length node.metrics['metric_git_number_authors'] = Object.keys(value).length } } }); } } function mainContributor(obj={}, asc=true) { let biggestChurn = 0 let authorBiggestChurn = '' for (let key in obj) { if (obj[key] > biggestChurn) { biggestChurn = obj[key] authorBiggestChurn = key } } return authorBiggestChurn } // daterangepicker for git date range function initDateRangeUI() { $('input[name="daterange"]').daterangepicker({ "startDate": commit_first_date, "endDate": commit_last_date, "minDate": commit_first_date, "maxDate": commit_last_date, // ranges: { // 'Last 3 days': [moment().subtract(2, 'days'), moment()], // 'Last 10 days': [moment().subtract(9, 'days'), moment()], // 'Last 30 days': [moment().subtract(29, 'days'), moment()], // 'Last 60 days': [moment().subtract(59, 'days'), moment()], // 'This month': [moment().startOf('month'), moment().endOf('month')], // 'Last month': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')] // }, isInvalidDate: function(date) { if ( commit_dates.includes(date.format('DD/MM/YYYY')) ) { return false } else { return true } }, "locale": { "format": "DD/MM/YYYY", "separator": " - ", "applyLabel": "Apply", "cancelLabel": "Cancel", "fromLabel": "From", "toLabel": "To", "customRangeLabel": "Custom", "weekLabel": "W", "daysOfWeek": [ "Su", "Mo", "Tu", "We", "Th", "Fr", "Sa" ], "monthNames": [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ], "firstDay": 1 }, opens: 'left' }, function(start, end, label) { console.log("A new date selection was made: " + start.format('DD/MM/YYYY') + ' to ' + end.format('DD/MM/YYYY')); dateRangePickerFrom = start.format('DD/MM/YYYY') dateRangePickerTo = end.format('DD/MM/YYYY') // console.log(dateRangePickerFrom) // console.log(dateRangePickerTo) initGitMetricsForDateRange() showToastDateRangeUpdate() }) } function showToastDateRangeUpdate() { const toastLiveExample = document.getElementById('toastDateRangeUpdated') const toast = new bootstrap.Toast(toastLiveExample) toast.show() } // Copyright 2021 Observable, Inc. // Released under the ISC license. // https://observablehq.com/@d3/multi-line-chart function LineChart(data, { x = ([x]) => x, // given d in data, returns the (temporal) x-value y = ([, y]) => y, // given d in data, returns the (quantitative) y-value z = () => 1, // given d in data, returns the (categorical) z-value title, // given d in data, returns the title text defined, // for gaps in data curve = d3.curveLinear, // method of interpolation between points marginTop = 20, // top margin, in pixels marginRight = 30, // right margin, in pixels marginBottom = 30, // bottom margin, in pixels marginLeft = 40, // left margin, in pixels width = 640, // outer width, in pixels height = 400, // outer height, in pixels xType = d3.scaleUtc, // type of x-scale xDomain, // [xmin, xmax] xRange = [marginLeft, width - marginRight], // [left, right] yType = d3.scaleLinear, // type of y-scale yDomain, // [ymin, ymax] yRange = [height - marginBottom, marginTop], // [bottom, top] yFormat, // a format specifier string for the y-axis yLabel, // a label for the y-axis zDomain, // array of z-values color = "currentColor", // stroke color of line, as a constant or a function of *z* strokeLinecap, // stroke line cap of line strokeLinejoin, // stroke line join of line strokeWidth = 1.0, // stroke width of line strokeOpacity, // stroke opacity of line mixBlendMode = "multiply", // blend mode of lines voronoi, // show a Voronoi overlay? (for debugging) id } = {}) { // Compute values. let textColor = "#FFF" if (!darkMode) { textColor = "#333333"} const X = d3.map(data, x); const Y = d3.map(data, y); const Z = d3.map(data, z); const O = d3.map(data, d => d); if (defined === undefined) defined = (d, i) => !isNaN(X[i]) && !isNaN(Y[i]) ; const D = d3.map(data, defined); // Compute default domains, and unique the z-domain. if (xDomain === undefined) xDomain = d3.extent(X); if (yDomain === undefined) yDomain = [0, d3.max(Y, d => typeof d === "string" ? +d : d)]; if (zDomain === undefined) zDomain = Z; zDomain = new d3.InternSet(zDomain); // Omit any data not present in the z-domain. const I = d3.range(X.length).filter(i => zDomain.has(Z[i])); // Construct scales and axes. const xScale = xType(xDomain, xRange); const yScale = yType(yDomain, yRange); const xAxis = d3.axisBottom(xScale).ticks(width / 80).tickSizeOuter(0); const yAxis = d3.axisLeft(yScale).ticks(height / 60, yFormat); // Compute titles. const T = title === undefined ? Z : title === null ? null : d3.map(data, title); // Construct a line generator. const line = d3.line() .defined(i => D[i]) .curve(curve) .x(i => xScale(X[i])) .y(i => yScale(Y[i])); const svg = d3.create("svg") .attr("id", id) .attr("width", width) .attr("height", height) .attr("viewBox", [0, 0, width, height]) .attr("style", "max-width: 100%; height: auto; height: intrinsic;") .style("-webkit-tap-highlight-color", "transparent") .on("pointerenter", pointerentered) .on("pointermove", pointermoved) .on("pointerleave", pointerleft) .on("touchstart", event => event.preventDefault()); // An optional Voronoi display (for fun). if (voronoi) svg.append("path") .attr("fill", "none") .attr("stroke", "#ccc") .attr("d", d3.Delaunay .from(I, i => xScale(X[i]), i => yScale(Y[i])) .voronoi([0, 0, width, height]) .render()); svg.append("g") .attr("transform", `translate(0,${height - marginBottom})`) .call(xAxis); svg.append("g") .attr("transform", `translate(${marginLeft},0)`) .call(yAxis) .call(g => g.select(".domain").remove()) .call(voronoi ? () => {} : g => g.selectAll(".tick line").clone() .attr("x2", width - marginLeft - marginRight) .attr("stroke-opacity", 0.1)) .call(g => g.append("text") .attr("x", -marginLeft) .attr("y", 10) .attr("fill", "currentColor") .attr("text-anchor", "start") .text(yLabel)); const path = svg.append("g") .attr("fill", "none") .attr("stroke", typeof color === "string" ? color : null) .attr("stroke-linecap", strokeLinecap) .attr("stroke-linejoin", strokeLinejoin) .attr("stroke-width", strokeWidth) .attr("stroke-opacity", strokeOpacity) .selectAll("path") .data(d3.group(I, i => Z[i])) .join("path") //.style("mix-blend-mode", mixBlendMode) .attr("stroke", typeof color === "function" ? ([z]) => color(z) : null) .attr("d", ([, I]) => line(I)); const dot = svg.append("g") .attr("display", "none") .attr("fill", "red"); dot.append("circle") .attr("r", 2.5); dot.append("text") .attr("font-family", "sans-serif") .attr("font-size", 10) .attr("text-anchor", "middle") .attr("fill", textColor) .attr("y", -8); function pointermoved(event) { let strokeColor = "#323232" if (!darkMode) { strokeColor = "#828282"} const [xm, ym] = d3.pointer(event); const i = d3.least(I, i => Math.hypot(xScale(X[i]) - xm, yScale(Y[i]) - ym)); // closest point path.style("stroke", ([z]) => Z[i] === z ? null : strokeColor).filter(([z]) => Z[i] === z).raise(); dot.attr("transform", `translate(${xScale(X[i])},${yScale(Y[i])})`); if (T) dot.select("text").text(T[i]); svg.property("value", O[i]).dispatch("input", {bubbles: true}); } function pointerentered() { path.style("mix-blend-mode", null).style("stroke", "lightyellow"); dot.attr("display", null); } function pointerleft() { //path.style("mix-blend-mode", mixBlendMode).style("stroke", null); path.style("stroke", "lightsteelblue") dot.attr("display", "none"); svg.node().value = null; svg.dispatch("input", {bubbles: true}); } return Object.assign(svg.node(), {value: null}); } function generateTimeSeriesChart() { let timeSeriesComplexityTotal = {} let timeSeriesSlocTotal = {} let timeSeriesChurnTotal = {} let timeSeriesComplexity = [] let timeSeriesSloc = [] let timeSeriesChurn = [] for (let i = gitMetricsIndexFrom; i < gitMetricsIndexTo; i++) { // prepare complexity data for chart for (const [key, value] of Object.entries(commit_metrics[i].ws_complexity)) { let filter = true if (Object.keys(selectedNodesMap).length > 0) { for (const [selectedNode, v] of Object.entries(selectedNodesMap)) { if (selectedNode.includes(key.toLowerCase())) { filter = false } } } else { filter = false } if (filter == true) { continue } for (const [file, complexity] of Object.entries(timeSeriesComplexityTotal)) { if (file !== key) { timeSeriesComplexity.push( { 'filepath' : file, 'wscomplexity' : complexity, 'date': commit_metrics[i].exact_date.replace(/_/g, "-") } ) } } timeSeriesComplexityTotal[key] = value let timeSeriesComplexityEntry = { 'filepath' : key, 'wscomplexity' : value, 'date': commit_metrics[i].exact_date.replace(/_/g, "-") // TODO ... } timeSeriesComplexity.push(timeSeriesComplexityEntry) } // prepare sloc data for chart for (const [key, value] of Object.entries(commit_metrics[i].sloc)) { let filter = true if (Object.keys(selectedNodesMap).length > 0) { for (const [selectedNode, v] of Object.entries(selectedNodesMap)) { if (selectedNode.includes(key.toLowerCase())) { filter = false } } } else { filter = false } if (filter == true) { continue } for (const [file, sloc] of Object.entries(timeSeriesSlocTotal)) { if (file !== key) { timeSeriesSloc.push( { 'filepath' : file, 'sloc' : sloc, 'date': commit_metrics[i].exact_date.replace(/_/g, "-") } ) } } timeSeriesSlocTotal[key] = value let timeSeriesSlocEntry = { 'filepath' : key, 'sloc' : value, 'date': commit_metrics[i].exact_date.replace(/_/g, "-") // TODO ... } timeSeriesSloc.push(timeSeriesSlocEntry) } // prepare churn data for chart for (const [key, value] of Object.entries(commit_metrics[i].churn)) { let filter = true if (Object.keys(selectedNodesMap).length > 0) { for (const [selectedNode, v] of Object.entries(selectedNodesMap)) { if (selectedNode.includes(key.toLowerCase())) { filter = false } } } else { filter = false } if (filter == true) { continue } for (const [file, churn] of Object.entries(timeSeriesChurnTotal)) { if (file !== key) { timeSeriesChurn.push( { 'filepath' : file, 'churn' : churn, 'date': commit_metrics[i].exact_date.replace(/_/g, "-") } ) } } timeSeriesChurnTotal[key] = value let timeSeriesChurnEntry = { 'filepath' : key, 'churn' : value, 'date': commit_metrics[i].exact_date.replace(/_/g, "-") // TODO ... } timeSeriesChurn.push(timeSeriesChurnEntry) } } let complexityChart = LineChart(timeSeriesComplexity, { x: d => Date.parse(d.date), y: d => d.wscomplexity, z: d => d.filepath, yLabel: "Whitespace Complexity", width: 1000, height: 300, color: "lightsteelblue", voronoi: false, id: "timeSeriesComplexityChart" }) let slocChart = LineChart(timeSeriesSloc, { x: d => Date.parse(d.date), y: d => d.sloc, z: d => d.filepath, yLabel: "SLOC", width: 1000, height: 300, color: "lightsteelblue", voronoi: false, id: "timeSeriesSlocChart" }) let churnChart = LineChart(timeSeriesChurn, { x: d => Date.parse(d.date), y: d => d.churn, z: d => d.filepath, yLabel: "Code churn", width: 1000, height: 300, color: "lightsteelblue", voronoi: false, id: "timeSeriesChurnChart" }) document.getElementById("my_dataviz").appendChild(complexityChart); document.getElementById("time_series_sloc").appendChild(slocChart); document.getElementById("my_dataviz2").appendChild(churnChart); } function generateChangeCouplingChart() { let flows = [] let locations = [] let locationId = 0 let locationColorMap = {} for (let i = gitMetricsIndexFrom; i < gitMetricsIndexTo; i++) { // prepare change coupling data for chart if (commit_metrics[i].links.length > 0) { for (link of commit_metrics[i].links) { const matchingSourceKey = link.source const matchingTargetKey = link.target let filter = true if (Object.keys(selectedNodesMap).length > 0) { for (const [selectedNode, v] of Object.entries(selectedNodesMap)) { if ( selectedNode.includes(matchingSourceKey.toLowerCase()) || selectedNode.includes(matchingTargetKey.toLowerCase()) ) { filter = false } } } else { filter = false } if (filter == true) { continue } if ( !(locations.find(e => e.name === matchingSourceKey)) ) { if ( !(matchingSourceKey in locationColorMap) ) { let randomColor = "#000000".replace(/0/g,function(){return (~~(Math.random()*16)).toString(16);}); locationColorMap[matchingSourceKey] = randomColor } let location = { 'id': locationId, 'name': matchingSourceKey, 'color': locationColorMap[matchingSourceKey] } locations.push(location) let flow = { 'from' : locationId, 'to' : locationId, 'quantity': 0 } flows.push(flow) locationId += 1 } if ( !(locations.find(e => e.name === matchingTargetKey)) ) { if ( !(matchingTargetKey in locationColorMap) ) { // find the corresponding node color for (const [key, value] of Object.entries(nodeColorMap)) { if (key.includes(matchingTargetKey)) { locationColorMap[matchingTargetKey] = value } } // if (matchingTargetKey in nodeColorMap) { // locationColorMap[matchingTargetKey] = nodeColorMap[matchingTargetKey] // } else { // console.log("should not happen") // locationColorMap[matchingTargetKey] = '#FFFFFF' // "#000000".replace(/0/g,function(){return (~~(Math.random()*16)).toString(16);}); // } } let location = { 'id': locationId, 'name': matchingTargetKey, 'color': locationColorMap[matchingTargetKey] } locations.push(location) let flow = { 'from' : locationId, 'to' : locationId, 'quantity': 0 } flows.push(flow) locationId += 1 } const iSource = locations.findIndex(e => e.name === matchingSourceKey); const iTarget = locations.findIndex(e => e.name === matchingTargetKey); if (iSource > -1 && iTarget > -1) { let flow = { 'from' : locations[iSource].id, 'to' : locations[iTarget].id, 'quantity': 15 } flows.push(flow) } } } } for (let n = 0; n < locationId; n++) { for (let m = 0; m < locationId; m++) { if ( !(flows.find(e => e.from === n && e.to === m )) ) { let flow = { 'from' : n, 'to' : m, 'quantity': 0 } flows.push(flow) } } } // Borrowed from this great blog entry: // https://blog.noser.com/d3-js-chord-diagramm-teil-2-benutzerdefinierte-sortierung-und-kurvenformen/ var matrix = []; //Map list of data to matrix flows.forEach(function (flow) { //Initialize sub-array if not yet exists if (!matrix[flow.to]) { matrix[flow.to] = []; } matrix[flow.to][flow.from] = flow.quantity; }); /*////////////////////////////////////////////////////////// /////////////// Initiate Chord Diagram ///////////////////// //////////////////////////////////////////////////////////*/ let size = 900; let dr = 40; //radial translation for group names let dx = 20; //horizontal translation for group names let margin = { top: 0, right: 50, bottom: 50, left: 50 }; let chordWidth = (size + 200) - margin.left - margin.right; let chordHeight = size - margin.top - margin.bottom; let innerRadius = Math.min(chordWidth, chordHeight) * .39; let outerRadius = innerRadius * 1.08; let root = d3.select("#change_coupling_chord_diagram"); //Generate tooltip already, but keep it invisible for now. var toolTip = root.append("div") .classed("tooltip", true) .style("opacity", 0) .style("position", "absolute") .style("text-align", "center") .style("padding", "6px") .style("font", "10px sans-serif") .style("color", "black") .style("background", "silver") .style("border", "1px solid gray") .style("border-radius", "8px") .style("pointer-events", "none"); var focusedChordGroupIndex = null; /*Initiate the SVG*/ //D3.js v3! var svg = root.append("svg:svg") .attr("width", chordWidth + margin.left + margin.right) .attr("height", chordHeight + margin.top + margin.bottom) .attr("id", "svg_change_coupling_chord_diagram"); var container = svg.append("g") .attr("transform", "translate(" + (margin.left + chordWidth / 2) + "," + (margin.top + chordHeight / 2) + ")"); var chord = customChordLayout() .padding(0.04) .sortSubgroups(d3.descending) /*sort the chords inside an arc from high to low*/ .sortChords(d3.ascending) /*which chord should be shown on top when chords cross. Now the largest chord is at the top*/ .matrix(matrix); /*////////////////////////////////////////////////////////// ////////////////// Draw outer Arcs ///////////////////////// //////////////////////////////////////////////////////////*/ var arc = d3.arc() .innerRadius(innerRadius) .outerRadius(outerRadius); var g = container.selectAll("g.group") .data(chord.groups) .enter() .append("svg:g") .attr("class", function (d) { return "group group-" + locations[d.index].id; }); g.append("svg:path") .attr("d", arc) .style("fill", function (d) { return locations[d.index].color; }) .style("stroke", function (d) { return d3.rgb(locations[d.index].color).brighter(); }) .on("click", function (event, d) { highlightChords(d.index) }) // .on("click", function (d) { highlightChords(d.index) }) .on("mouseover", function(event, i) { showArcToolTip(event, i); }) .on("mouseout", function(d) { hideToolTip() }); /*////////////////////////////////////////////////////////// //////////////// Initiate inner chords ///////////////////// //////////////////////////////////////////////////////////*/ var chords = container.selectAll("path.chord") .data(chord.chords) .enter() .append("svg:path") .attr("class", function (d) { return "chord chord-source-" + d.source.index + " chord-target-" + d.target.index; }) .attr("d", customChordPathGenerator().radius(innerRadius)) //Change the fill to reference the unique gradient ID //of the source-target combination .style("fill", function (d) { return "url(#chordGradient-" + d.source.index + "-" + d.target.index + ")"; }) .style("stroke", function (d) { return "url(#chordGradient-" + d.source.index + "-" + d.target.index + ")"; }) .style("fill-opacity", "0.7") .on("mouseover", function(event, i) { if (focusedChordGroupIndex === null || i.source.index === focusedChordGroupIndex || i.target.index === focusedChordGroupIndex) { if (focusedChordGroupIndex === null) { d3.selectAll(".chord") .style("fill-opacity", 0.2) .style("stroke-opacity", 0.2); d3.select(this).style("fill-opacity", 1); } else { d3.selectAll(".chord.chord-source-" + focusedChordGroupIndex + ", " + ".chord.chord-target-" + focusedChordGroupIndex) .style("fill-opacity", 0.2) .style("stroke-opacity", 0.2); d3.select(this).style("fill-opacity", 1); } showChordToolTip(event, i); } }) .on("mouseout", function(d) { if (focusedChordGroupIndex === null) { d3.selectAll(".chord") .style("fill-opacity", 0.7) .style("stroke-opacity", 1); } else { d3.selectAll(".chord.chord-source-" + focusedChordGroupIndex + ", " + ".chord.chord-target-" + focusedChordGroupIndex) .style("fill-opacity", 0.7) .style("stroke-opacity", 1); } hideToolTip(); }); //Cf https://www.visualcinnamon.com/2016/06/orientation-gradient-d3-chord-diagram //Create a gradient definition for each chord var grads = svg.append("defs").selectAll("linearGradient") .data(chord.chords) .enter().append("linearGradient") //Create a unique gradient id per chord: e.g. "chordGradient-0-4" .attr("id", function (d) { return "chordGradient-" + d.source.index + "-" + d.target.index; }) //Instead of the object bounding box, use the entire SVG for setting locations //in pixel locations instead of percentages (which is more typical) .attr("gradientUnits", "userSpaceOnUse") //The full mathematical formula to find the x and y locations .attr("x1", function (d, i) { return innerRadius * Math.cos((d.source.endAngle - d.source.startAngle) / 2 + d.source.startAngle - Math.PI / 2); }) .attr("y1", function (d, i) { return innerRadius * Math.sin((d.source.endAngle - d.source.startAngle) / 2 + d.source.startAngle - Math.PI / 2); }) .attr("x2", function (d, i) { return innerRadius * Math.cos((d.target.endAngle - d.target.startAngle) / 2 + d.target.startAngle - Math.PI / 2); }) .attr("y2", function (d, i) { return innerRadius * Math.sin((d.target.endAngle - d.target.startAngle) / 2 + d.target.startAngle - Math.PI / 2); }); //Set the starting color (at 0%) grads.append("stop") .attr("offset", "0%") .attr("stop-color", function (d) { return locations[d.source.index].color; }); //Set the ending color (at 100%) grads.append("stop") .attr("offset", "100%") .attr("stop-color", function (d) { return locations[d.target.index].color; }); /*////////////////////////////////////////////////////////// ////////////////// Initiate Ticks ////////////////////////// //////////////////////////////////////////////////////////*/ var ticks = g.append("svg:g") .selectAll("g.ticks") .data(groupTicks) .enter().append("svg:g") .attr("transform", function (d) { return "rotate(" + (d.angle * 180 / Math.PI - 90) + ")" + "translate(" + outerRadius + 40 + ",0)"; }); /*Append the tick around the arcs*/ ticks.append("svg:line") .attr("x1", 1) .attr("y1", 0) .attr("x2", 6) .attr("y2", 0) .attr("class", "ticks") .style("stroke", "#FFF") .style("stroke-width", "1.5px"); let labelColor = "#FFF" if (!darkMode) { labelColor = "#333333"} /*Add the labels for the ticks*/ ticks.append("svg:text") .attr("class", "tickLabels") .attr("x", 12) .attr("dy", ".35em") .style("font-size", "10px") .style("font-family", "sans-serif") .attr("fill", labelColor) .attr("transform", function (d) { return d.angle > Math.PI ? "rotate(180)translate(-25)" : null; }) .style("text-anchor", function (d) { return d.angle > Math.PI ? "end" : null; }) //.text(function (d) { return d.label; }); /*////////////////////////////////////////////////////////// ////////////////// Initiate Names ////////////////////////// //////////////////////////////////////////////////////////*/ g.append("svg:text") .each(function (d) { d.angle = (d.startAngle + d.endAngle) / 2; }) .attr("dy", ".35em") .attr("class", "titles") .style("font-size", "10px") .style("font-family", "sans-serif") .attr("fill", labelColor) .attr("text-anchor", function (d) { return d.angle > Math.PI ? "end" : null; }) .attr("transform", function (d) { var r = outerRadius + dr; var angle = d.angle + ((3 *Math.PI) / 2); var x = r * Math.cos(angle); var y = r * Math.sin(angle); if (d.angle > Math.PI) { x -= dx; } else { x += dx; } return "translate(" + x + ", " + y + ")"; }) .text(function (d, i) { if (locations[i].name.includes("/")) { return locations[i].name.substring(locations[i].name.lastIndexOf('/') + 1) } else { return locations[i].name } }); /*Lines from labels to arcs*/ /*part in radial direction*/ this.g.append("line") .attr("x1", function (d) { return outerRadius * Math.cos(d.angle + ((3 * Math.PI) / 2)); }) .attr("y1", function (d) { return outerRadius * Math.sin(d.angle + ((3 * Math.PI) / 2)); }) .attr("x2", function (d) { return (outerRadius + dr) * Math.cos(d.angle + ((3 * Math.PI) / 2)); }) .attr("y2", function (d) { return (outerRadius + dr) * Math.sin(d.angle + ((3 * Math.PI) / 2)); }) .style("stroke", "#FFF") .style("stroke-width", "0.5px"); /*horizontal part*/ this.g.append("line") .attr("x1", function (d) { return (outerRadius + dr) * Math.cos(d.angle + ((3 * Math.PI) / 2)); }) .attr("y1", function (d) { return (outerRadius + dr) * Math.sin(d.angle + ((3 * Math.PI) / 2)); }) .attr("x2", function (d) { var x = (outerRadius + dr) * Math.cos(d.angle + ((3 * Math.PI) / 2)); if (d.angle > Math.PI) { x -= dx - 5; } else { x += dx - 5; } return x; }) .attr("y2", function (d) { return (outerRadius + dr) * Math.sin(d.angle + ((3 * Math.PI) / 2)); }) .style("stroke", "#FFF") .style("stroke-width", "0.5px"); /*////////////////////////////////////////////////////////// ////////////////// Extra Functions ///////////////////////// //////////////////////////////////////////////////////////*/ /*Returns an array of tick angles and labels, given a group*/ function groupTicks(d) { var anglePerPerson = (d.endAngle - d.startAngle) / d.value; return d3.range(0, d.value, 100).map(function (v, i) { return { angle: v * anglePerPerson + d.startAngle, label: i % 5 ? null : v //Each 5th tick has a label }; }); }; //Hides all chords except the chords connecting to the subgroup / //location of the given index. function highlightChords(index) { //If this subgroup is already highlighted, toggle all chords back on. if (focusedChordGroupIndex === index) { showAllChords(); return; } hideAllChords(); //Show only the ones with source or target == index d3.selectAll(".chord-source-" + index + ", .chord-target-" + index) .transition().duration(500) .style("fill-opacity", "0.7") .style("stroke-opacity", "1"); focusedChordGroupIndex = index; }; function showAllChords() { svg.selectAll("path.chord") .transition().duration(500) .style("fill-opacity", "0.7") .style("stroke-opacity", "1"); focusedChordGroupIndex = null; }; function hideAllChords() { svg.selectAll("path.chord") .transition().duration(500) .style("fill-opacity", "0") .style("stroke-opacity", "0"); }; function showChordToolTip(event, chord) { var prompt = ""; // if (chord.source.index !== chord.target.index) { // prompt += chord.source.value + " Kunden gingen von " + // locations[chord.target.index].name + " nach " + // locations[chord.source.index].name + "."; // prompt += "<br>"; // prompt += chord.target.value + " Kunden gingen von " + // locations[chord.source.index].name + " nach " + // locations[chord.target.index].name + "."; // } // else { // prompt += chord.source.value + " Kunden blieben in " + // locations[chord.source.index].name + "."; // } prompt += locations[chord.target.index].name + "<br>" + "... changed together with ... " + "<br>" + locations[chord.source.index].name + "."; const[x, y] = d3.pointer(event); toolTip .style("opacity", 1) .style("font-size", "10px") .html(prompt) .style("left", x - toolTip.node().getBoundingClientRect().width / 32 + "px") // .style("left", d3.event.pageX - toolTip.node().getBoundingClientRect().width / 2 + "px") .style("top", y + 300 + "px"); // .style("top", (d3.event.pageY - 50) + "px"); }; function showArcToolTip(event, arc) { const[x, y] = d3.pointer(event); // console.log(locations) // console.log(arc) var prompt = locations[arc.index].name + "."; //Math.round(arc.value) toolTip .style("opacity", 1) .html(prompt) .style("left", x + toolTip.node().getBoundingClientRect().width + "px") .style("top", y + 300 + "px"); }; function hideToolTip() { toolTip.style("opacity", 0); }; //////////////////////////////////////////////////////////// //////////// Custom Chord Layout Function ////////////////// /////// Places the Chords in the visually best order /////// ///////////////// to reduce overlap //////////////////////// //////////////////////////////////////////////////////////// //////// Slightly adjusted by Nadieh Bremer //////////////// //////////////// VisualCinnamon.com //////////////////////// //////////////////////////////////////////////////////////// ////// Original from the d3.layout.chord() function //////// ///////////////// from the d3.js library /////////////////// //////////////// Created by Mike Bostock /////////////////// //////////////////////////////////////////////////////////// function customChordLayout() { var ε = 1e-6, ε2 = ε * ε, π = Math.PI, τ = 2 * π, τε = τ - ε, halfπ = π / 2, d3_radians = π / 180, d3_degrees = 180 / π; var chord = {}, chords, groups, matrix, n, padding = 0, sortGroups, sortSubgroups, sortChords; function relayout() { var subgroups = {}, groupSums = [], groupIndex = d3.range(n), subgroupIndex = [], k, x, x0, i, j; var numSeq; chords = []; groups = []; k = 0, i = -1; while (++i < n) { x = 0, j = -1, numSeq = []; while (++j < n) { x += matrix[i][j]; } groupSums.push(x); ////////////////////////////////////// ////////////// New part ////////////// ////////////////////////////////////// for (var m = 0; m < n; m++) { numSeq[m] = (n + (i - 1) - m) % n; } subgroupIndex.push(numSeq); ////////////////////////////////////// ////////// End new part ///////////// ////////////////////////////////////// k += x; }//while k = (τ - padding * n) / k; x = 0, i = -1; while (++i < n) { x0 = x, j = -1; while (++j < n) { var di = groupIndex[i], dj = subgroupIndex[di][j], v = matrix[di][dj], a0 = x, a1 = x += v * k; subgroups[di + "-" + dj] = { index: di, subindex: dj, startAngle: a0, endAngle: a1, value: v }; }//while groups[di] = { index: di, startAngle: x0, endAngle: x, value: (x - x0) / k }; x += padding; }//while i = -1; while (++i < n) { j = i - 1; while (++j < n) { var source = subgroups[i + "-" + j], target = subgroups[j + "-" + i]; if (source.value || target.value) { chords.push(source.value < target.value ? { source: target, target: source } : { source: source, target: target }); }//if }//while }//while if (sortChords) resort(); }//function relayout function resort() { chords.sort(function (a, b) { return sortChords((a.source.value + a.target.value) / 2, (b.source.value + b.target.value) / 2); }); } chord.matrix = function (x) { if (!arguments.length) return matrix; n = (matrix = x) && matrix.length; chords = groups = null; return chord; }; chord.padding = function (x) { if (!arguments.length) return padding; padding = x; chords = groups = null; return chord; }; chord.sortGroups = function (x) { if (!arguments.length) return sortGroups; sortGroups = x; chords = groups = null; return chord; }; chord.sortSubgroups = function (x) { if (!arguments.length) return sortSubgroups; sortSubgroups = x; chords = null; return chord; }; chord.sortChords = function (x) { if (!arguments.length) return sortChords; sortChords = x; if (chords) resort(); return chord; }; chord.chords = function () { if (!chords) relayout(); return chords; }; chord.groups = function () { if (!groups) relayout(); return groups; }; return chord; }; //////////////////////////////////////////////////////////// //////////// Custom Chord Path Generator /////////////////// ///////// Uses cubic bezier curves with quadratic ////////// /////// spread of control points to minimise overlap ///////