简体   繁体   中英

How to normalize data in d3 and plot the lines in the axis independent of other line ranges

I have a parallel coordinates plot visualized in d3.js, my problem lies within the data, the different lines for "car" "bicycle" "boat" and "airplane" have different ranges of numbers, therefore the lines that have low numbers can't be seen and are represented as a flat line instead. This ocurs with "car" line for example because the airplane data is much greater.

I can't seem to figure out how can one normalize data so that when "type" is "car" then normalize from 0 to 1, and when type is "bicycle" or "boat" normalize with 0 to max. But show these data in the axis independent of the others. As you can see the axis don't have labels for ranges. Instead I plan to show the exact data when hovering over a line.

This is the d3 code:

 var dataSet = [{ "type": "car", "first": 0.65, "second": 0.34, "third": 0.55, "fourth": 0.39 }, { "type": "car", "dataset": "train", "first": 0.59, "second": 0.33, "third": 0.50, "fourth": 0.40 }, { "type": "bicycle", "first": 200, "second": 230, "third": 250, "fourth": 300 }, { "type": "bicycle", "dataset": "train", "first": 200, "second": 280, "third": 225, "fourth": 278 }, { "type": "boat", "first": 320, "second": 324, "third": 532, "fourth": 321 }, { "type": "boat", "dataset": "train", "first": 128, "second": 179, "third": 166, "fourth": 234 }, { "type": "airplane", "first": 1500, "second": 2000, "third": 2321, "fourth": 1793 }, { "type": "airplane", "dataset": "train", "first": 1438, "second": 2933, "third": 2203, "fourth": 2000 } ] var margin = { top: 5, right: 50, bottom: 5, left: 70 }, width = 600 - margin.left - margin.right, height = 280 - margin.top - margin.bottom; var dimensions = [{ name: "type", scale: d3.scale.ordinal().rangePoints([0, height]), type: "string" }, { name: "first", scale: d3.scale.linear().range([height, 0]), type: "number" }, { name: "second", scale: d3.scale.linear().range([height, 0]), type: "number" }, { name: "third", scale: d3.scale.linear().range([height, 0]), type: "number" }, { name: "fourth", scale: d3.scale.linear().range([height, 0]), type: "number" } ]; var maxRange = d3.max(dataSet, function(d) { return Math.max(d.first, d.second, d.third, d.fourth); }); var x = d3.scale.ordinal() .domain(dimensions.map(function(d) { return d.name; })) .rangePoints([0, width]); var line = d3.svg.line() .defined(function(d) { return !isNaN(d[1]); }); // CREATE A COLOR SCALE var color = d3.scale.ordinal() .range(["#4683b8", "#79add2", "#a6c9de", "#cadbed", "#9d9bc4", "#bcbed9", "#dadaea", "#f6d2a8", "#f2b076", "#ef914e", "#d65e2a"]) var yAxis = d3.svg.axis() .orient("left"); var svg = d3.select("#parallel_coor") .append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); var dimension = svg.selectAll(".dimension") .data(dimensions) .enter().append("g") .attr("class", "dimension") .attr("transform", function(d) { return "translate(" + x(d.name) + ")"; }); function parallel(dataSet) { dimensions.forEach(function(dimension) { dimension.scale.domain(dimension.type === "number" ? ([0, maxRange]) : dataSet.map(function(d) { return d[dimension.name]; })); }); svg.append("g") .attr("class", "background coorPath") .selectAll("path") .data(dataSet) .enter().append("path") .attr("d", draw); // USE THE COLOR SCALE TO SET THE STROKE BASED ON THE DATA foreground = svg.append("g") .attr("class", "foreground coorPath") .selectAll("path") .data(dataSet) .enter() .append("path") .attr("d", draw) .style("stroke", function(d) { var company = d.type.slice(0, d.type.indexOf(' ')); return color(company); }) dimension.append("g") .attr("class", "axis") .each(function(d) { d3.select(this).call(yAxis.scale(d.scale)); }) // remove axis numbers svg.selectAll(".tick") .each(function(d) { if (typeof d == 'number') { this.remove(); } }); var ordinal_labels = svg.selectAll(".axis text") .on("mouseover", mouseover) .on("mouseout", mouseout); var projection = svg.selectAll(".background path,.foreground path") .on("mouseover", mouseover) .on("mouseout", mouseout); // d3.selectAll("[dataset=train]").attr("visibility", "hidden"); let trainline = d3.selectAll(".coorPath path").filter(function(d) { return d.dataset == "train"; }) .attr("visibility", "hidden"); // On Click, we want to add data to the array and chart let lines = svg.selectAll(".coorPath path").on("mouseover", function(d) { // show train when click others trainline.attr("visibility", "visible") trainline.style("stroke-dasharray", ("5, 5")) lines.style('opacity', function(e) { return e.type === d.type ? 1 : 0.2; }); }); // On Click, we want to add data to the array and chart lines.on("mouseout", function(d) { // show train when click others lines.style('opacity', null); trainline.attr("visibility", "hidden") }); // making parallel coordinates plot interactive to highlight lines function mouseover(d) { svg.classed("active", true); if (typeof d === "string") { projection.classed("inactive", function(p) { return p.name !== d; }); projection.filter(function(p) { return p.name === d; }).each(moveToFront); ordinal_labels.classed("inactive", function(p) { return p !== d; }); ordinal_labels.filter(function(p) { return p === d; }).each(moveToFront); } else { projection.classed("inactive", function(p) { return p !== d; }); projection.filter(function(p) { return p === d; }).each(moveToFront); ordinal_labels.classed("inactive", function(p) { return p !== d.name; }); ordinal_labels.filter(function(p) { return p === d.name; }).each(moveToFront); } } function mouseout(d) { svg.classed("active", false); projection.classed("inactive", false); ordinal_labels.classed("inactive", false); } function moveToFront() { this.parentNode.appendChild(this); } function draw(d) { return line(dimensions.map(function(dimension) { return [x(dimension.name), dimension.scale(d[dimension.name])]; })); } } parallel(dataSet);
 svg { font: 12px sans-serif; } .background path { fill: none; stroke: none; stroke-width: 20px; pointer-events: stroke; } .foreground path { fill: none; stroke: steelblue; stroke-width: 1.5px; } .axis .title { font-size: 11px; font-weight: bold; text-transform: uppercase; } .axis line, .axis path { fill: none; stroke: #000; shape-rendering: crispEdges; } .axis.string { font-size: 6px; } .label { -webkit-transition: fill 125ms linear; } .active .label:not(.inactive) { font-weight: bold; font-size: 11px; } .label.inactive { fill: #ccc; } .foreground path.inactive { stroke: #ccc; stroke-opacity: .2; stroke-width: 1.2px; } /* body { background-color: grey; } */ .node circle { fill: #fff; stroke: steelblue; stroke-width: 2px; } .node circle.highlighted { stroke: #00477F; stroke-width: 5px; } .node text { font: 12px sans-serif; } .link { fill: none; stroke: #ccc; stroke-width: 2px; } { box-sizing: border-box; } /* Create four equal columns that floats next to each other */ #svg0 { float: left; width: 120px; margin: 15px; padding: 20px; } #svg1 { float: left; width: auto; length: auto; border: 0.8px solid grey; margin: 5px; margin-top: 10px; } #svg2 { float: left; width: auto; length: auto; border: 0.8px solid grey; margin: 5px; margin-top: 10px; } #svg3 { float: left; width: auto; length: auto; border: 0.8px solid grey; margin: 5px; margin-top: 10px; } #svg4 { float: left; width: auto; length: auto; border: 0.8px solid grey; margin: 5px; margin-top: 10px; } #svg5 { float: left; width: auto; length: auto; border: 0.8px solid grey; margin: 5px; margin-top: 10px; } #svg6 { float: left; width: auto; length: auto; border: 0.8px solid grey; margin: 5px; margin-top: 10px; } #svg7 { float: left; width: auto; length: auto; border: 0.8px solid grey; margin: 5px; margin-top: 10px; } #svg8 { float: left; width: auto; length: auto; border: 0.8px solid grey; margin: 5px; margin-top: 10px; } #svg9 { float: left; width: auto; length: auto; border: 0.8px solid grey; margin: 5px; margin-top: 10px; } .treeClass { width: 5000px; height: 300px; } /* Clear floats after the columns */ .treeClass:after { content: ""; display: table; clear: both; } #scatterplots { width: 3000px; } #parallel_coor { /* float: right; */ /* margin: 50px; */ /* margin-right: 40%; */ margin-top: 2px; margin-bottom: 2px; } #scatterplot { float: left; margin-left: 40px; /*margin: 20px;*/ } #ROCPlot { float: left; margin-right: 300px; margin-left: 300px; } svg rect.borderline { fill: white; stroke-width: 0.8; stroke: grey; } .circle { fill: red; } .dot { fill: black; stroke: grey; } .axis path, .axis line { fill: none; stroke: #aaa; shape-rendering: crispEdges; } .axis text { font-family: sans-serif; font-size: 11px; } ul { height: 28px; list-style-type: none; margin: -8px; padding: 0; overflow: hidden; background-color: #333; } li { float: left; border-right: 1px solid #bbb; } li:last-child { border-right: none; } li a { text-size: 10px; display: block; color: white; text-align: center; padding: 5px 7px; text-decoration: none; } li a:hover:not(.active) { background-color: #111; } .active { background-color: #1679af; }
 <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script> <div id="parallel_coor"></div>

any help or idea how to tackle this issue with javascript or d3.js would be highly appreciated. Thank you!

You've made things way more complicated than necessary, especially with the foreground/background logic. I've rewritten the logic to draw a group of lines per category, one dashed and one solid. The result is the same, only the data handling is easier:

 var dataSet = [{ "type": "car", "first": 0.65, "second": 0.34, "third": 0.55, "fourth": 0.39 }, { "type": "car", "dataset": "train", "first": 0.59, "second": 0.33, "third": 0.50, "fourth": 0.40 }, { "type": "bicycle", "first": 200, "second": 230, "third": 250, "fourth": 300 }, { "type": "bicycle", "dataset": "train", "first": 200, "second": 280, "third": 225, "fourth": 278 }, { "type": "boat", "first": 320, "second": 324, "third": 532, "fourth": 321 }, { "type": "boat", "dataset": "train", "first": 128, "second": 179, "third": 166, "fourth": 234 }, { "type": "airplane", "first": 1500, "second": 2000, "third": 2321, "fourth": 1793 }, { "type": "airplane", "dataset": "train", "first": 1438, "second": 2933, "third": 2203, "fourth": 2000 } ]; var processedData = []; dataSet.forEach(function(d) { var match = processedData.find(function(p) { return p.type === d.type; }); if(!match) { match = { type: d.type, }; processedData.push(match); } var values = [d.first, d.second, d.third, d.fourth]; if(d.dataset === "train") { match.train = values; } else { match.test = values; } }); processedData.forEach(function(d) { // Normalise the values in the arrays const min = Math.min(d3.min(d.train), d3.min(d.test)); const max = Math.max(d3.max(d.train), d3.max(d.test)); d.trainNormalised = d.train.map(function(v) { return (v - min) / (max - min); }); d.testNormalised = d.test.map(function(v) { return (v - min) / (max - min); }); }); var margin = { top: 5, right: 50, bottom: 5, left: 70 }, width = 600 - margin.left - margin.right, height = 280 - margin.top - margin.bottom; var categoryScale = d3.scale.ordinal() .domain(processedData.map(function(d) { return d.type; })) .rangePoints([0, height]); var y = d3.scale.linear() .domain([0, 1]) .range([height, 0]); var x = d3.scale.ordinal() .domain(d3.range(5)) .rangePoints([0, width]); var line = d3.svg.line() .defined(function(d) { return !isNaN(d[1]); }); // CREATE A COLOR SCALE var color = d3.scale.ordinal() .range(["#4683b8", "#79add2", "#a6c9de", "#cadbed", "#9d9bc4", "#bcbed9", "#dadaea", "#f6d2a8", "#f2b076", "#ef914e", "#d65e2a"]) var svg = d3.select("#parallel_coor") .append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); svg.selectAll(".dimension.axis") .data([categoryScale, y, y, y, y]) .enter() .append("g") .attr("class", "dimension axis") .attr("transform", function(d, i) { return "translate(" + x(i) + ")"; }) .each(function(d) { const yAxis = d3.svg.axis() .scale(d) .ticks([]) .orient("left"); d3.select(this).call(yAxis); }); function parallel(data) { // Draw one line group per type (car, boat) // Each line group consists of a train and a test line; var lineGroup = svg.append("g") .selectAll(".lineGroup") .data(data) .enter() .append("g") .attr("class", "lineGroup") .each(function(d) { if(d.train) d3.select(this).append("path") .datum([d, "train"]); if(d.test) d3.select(this).append("path") .datum(function(d) { return [d, "test"]; }); }) lineGroup .attr("stroke", function(d) { var company = d.type.slice(0, d.type.indexOf(' ')); return color(company); }) .selectAll("path") .attr("class", function(d) { return d[1]; }) .attr("d", draw); lineGroup .on("mouseover", function(d) { // show train when click others d3.select(this).classed("active", true); lineGroup .filter(function(e) { return e.type !== d.type; }) .style('opacity', 0.2); }) .on("mouseout", function(d) { d3.select(this).classed("active", false); lineGroup.style('opacity', null); }); function draw(d) { var data = d[0], type = d[1]; var points = data[type + "Normalised"].map(function(v, i) { return [x(i + 1), y(v)]; }); points.unshift([x(0), categoryScale(data.type)]); return line(points); } } parallel(processedData);
 svg { font: 12px sans-serif; } .lineGroup path { fill: none; } .lineGroup.active .train { visibility: visible; } .train { visibility: hidden; stroke-dasharray: 5 5; } .axis line, .axis path { fill: none; stroke: #000; shape-rendering: crispEdges; }
 <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script> <div id="parallel_coor"></div>


Edit to specify which categories to normalise together, you can add every type to an array, grouping types you want to normalise together.

Then, loop over the groups and calculate the common min and max values. The rest of the code is identical to the one above.

 var dataSet = [{ "type": "car", "first": 0.65, "second": 0.34, "third": 0.55, "fourth": 0.39 }, { "type": "car", "dataset": "train", "first": 0.59, "second": 0.33, "third": 0.50, "fourth": 0.40 }, { "type": "bicycle", "first": 200, "second": 230, "third": 250, "fourth": 300 }, { "type": "bicycle", "dataset": "train", "first": 200, "second": 280, "third": 225, "fourth": 278 }, { "type": "boat", "first": 320, "second": 324, "third": 532, "fourth": 321 }, { "type": "boat", "dataset": "train", "first": 128, "second": 179, "third": 166, "fourth": 234 }, { "type": "airplane", "first": 1500, "second": 2000, "third": 2321, "fourth": 1793 }, { "type": "airplane", "dataset": "train", "first": 1438, "second": 2933, "third": 2203, "fourth": 2000 } ]; var normaliseTogether = [ ["car"], ["bicycle", "boat"], ["airplane"], ]; var processedData = []; dataSet.forEach(function(d) { var match = processedData.find(function(p) { return p.type === d.type; }); if (!match) { match = { type: d.type, }; processedData.push(match); } var values = [d.first, d.second, d.third, d.fourth]; if (d.dataset === "train") { match.train = values; } else { match.test = values; } }); normaliseTogether.forEach(function(groups) { var groupData = processedData.filter(function(d) { return groups.includes(d.type); }); // Normalise the values in the arrays let min = Infinity, max = -Infinity; groupData.forEach(function(d) { min = Math.min(min, d3.min(d.train), d3.min(d.test)); max = Math.max(max, d3.max(d.train), d3.max(d.test)); }); groupData.forEach(function(d) { d.trainNormalised = d.train.map(function(v) { return (v - min) / (max - min); }); d.testNormalised = d.test.map(function(v) { return (v - min) / (max - min); }); }); }); var margin = { top: 5, right: 50, bottom: 5, left: 70 }, width = 600 - margin.left - margin.right, height = 280 - margin.top - margin.bottom; var categoryScale = d3.scale.ordinal() .domain(processedData.map(function(d) { return d.type; })) .rangePoints([0, height]); var y = d3.scale.linear() .domain([0, 1]) .range([height, 0]); var x = d3.scale.ordinal() .domain(d3.range(5)) .rangePoints([0, width]); var line = d3.svg.line() .defined(function(d) { return !isNaN(d[1]); }); // CREATE A COLOR SCALE var color = d3.scale.ordinal() .range(["#4683b8", "#79add2", "#a6c9de", "#cadbed", "#9d9bc4", "#bcbed9", "#dadaea", "#f6d2a8", "#f2b076", "#ef914e", "#d65e2a"]) var svg = d3.select("#parallel_coor") .append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); svg.selectAll(".dimension.axis") .data([categoryScale, y, y, y, y]) .enter() .append("g") .attr("class", "dimension axis") .attr("transform", function(d, i) { return "translate(" + x(i) + ")"; }) .each(function(d) { const yAxis = d3.svg.axis() .scale(d) .ticks([]) .orient("left"); d3.select(this).call(yAxis); }); function parallel(data) { // Draw one line group per type (car, boat) // Each line group consists of a train and a test line; var lineGroup = svg.append("g") .selectAll(".lineGroup") .data(data) .enter() .append("g") .attr("class", "lineGroup") .each(function(d) { if (d.train) d3.select(this).append("path") .datum([d, "train"]); if (d.test) d3.select(this).append("path") .datum(function(d) { return [d, "test"]; }); }) lineGroup .attr("stroke", function(d) { var company = d.type.slice(0, d.type.indexOf(' ')); return color(company); }) .selectAll("path") .attr("class", function(d) { return d[1]; }) .attr("d", draw); lineGroup .on("mouseover", function(d) { // show train when click others d3.select(this).classed("active", true); lineGroup .filter(function(e) { return e.type !== d.type; }) .style('opacity', 0.2); }) .on("mouseout", function(d) { d3.select(this).classed("active", false); lineGroup.style('opacity', null); }); function draw(d) { var data = d[0], type = d[1]; var points = data[type + "Normalised"].map(function(v, i) { return [x(i + 1), y(v)]; }); points.unshift([x(0), categoryScale(data.type)]); return line(points); } } parallel(processedData);
 svg { font: 12px sans-serif; } .lineGroup path { fill: none; } .lineGroup.active .train { visibility: visible; } .train { visibility: hidden; stroke-dasharray: 5 5; } .axis line, .axis path { fill: none; stroke: #000; shape-rendering: crispEdges; }
 <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script> <div id="parallel_coor"></div>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM