D3
OG
Old school D3 from simpler times
All examples
By author
By category
About
ChandrakantThakkarDigiCorp
Full window
Github gist
Line Chart With Voronoi D3 V4
<!DOCTYPE html> <meta charset="utf-8"> <head> <title>Chandrakant Bharatkumar Thakkar</title> <link rel="stylesheet" href="./jquery-ui.css"> <link rel="stylesheet" href="./styles.css"> </head> <body> <div id="charts" style="width:800px;"> <div class="line-with-slider" style="width:800px;height:500px"> <div class="slider-component"> <div class="slider" style="margin-left:10px;margin-right:10px;"> <div id="slider-range-max" /> <div class="ticks" /> </div> </div> <div class="chart-svg"> <svg height="450" width="780"> </svg> </div> </div> </div> <a href="https://stackoverflow.com/users/7430694/chandrakant-thakkar" style="position: absolute;top: 87%;left: 77%;" target="_blank"> <img src="https://stackoverflow.com/users/flair/7430694.png" width="208" height="58" alt="profile for Chandrakant Thakkar at Stack Overflow, Q&A for professional and enthusiast programmers" title="profile for Chandrakant Thakkar at Stack Overflow, Q&A for professional and enthusiast programmers"> </a> <script src="./jquery-latest.min.js"></script> <script src="./jquery-ui.js"></script> <script src="./d3.v4.min.js"></script> <script src="./constant.js"></script> <script> let selectedSliderIndex = 5; let selectedCategory = chartData.category[selectedSliderIndex]; const granularity = 'daily'; var svg = d3.select("svg"), width = +svg.attr("width"), height = +svg.attr("height"); const margin = { left: 10, right: 10, bottom: 50, top: 30 }; let selectedDimensionValue = Object.entries(chartData.data)[0][0].toString().toLowerCase(); const ticksCalculationData = getTicksCalculation(chartData.category[0], chartData.category[chartData.category.length - 1], granularityToLabelMapping[granularity].granularityLabelForSlider, "0"); const xScale = d3.scalePoint() .domain(chartData.category) .range([0, width - (margin.left + margin.right)]); const prepareChartData = getPrepareChartData(chartData.data, chartData.category, chartData.measureFieldName); const ticksSize = 6; const { minValue, maxValue } = calculateMinMaxValue(prepareChartData.minMax[0], prepareChartData.minMax[1], false, ticksSize); const yScale = d3.scaleLinear().domain([minValue, maxValue]) .range([(height) - (margin.bottom + margin.top), 0]); const ticksValues = getTicksBalues(); function getTicksBalues() { let binSize = (maxValue - minValue) / (ticksSize - 1); binSize = Math.abs(binSize); const tickValues = []; let currentValue = minValue; while (currentValue <= maxValue) { tickValues.push(currentValue); currentValue = currentValue + binSize; } return tickValues; } const tickFormat = value => { return d3.timeFormat("%d-%b,%y")(new Date(value)); }; const tickYFormat = value => { return value + "%"; }; let sliderTicks = ''; ticksCalculationData.distinctTicks.forEach((data, index) => { sliderTicks = sliderTicks + '<div key={"ticks_" + index} class="ticks-text" style= "margin-left:' + parseFloat(xScale(chartData.category[index])).toFixed(2) + 'px;" data={' + xScale(chartData.category[index]) + '}>'; sliderTicks = sliderTicks + (Array.isArray(data.displayName) == true ? data.displayName.map((d, indexj) => '<div>' + d + '</div>') : [data.displayName]).join(''); sliderTicks = sliderTicks + '</div>'; }) sliderTicks = sliderTicks + '<div class="left-rect-div" style="width:' + ((xScale(chartData.category[selectedSliderIndex]) + margin.left) + "px") + ';left:' + ((-margin.left) + "px") + ';"}></div>'; d3.select(".ticks").html(""); d3.select(".ticks").html(sliderTicks); var yAxis = d3.axisLeft(yScale).ticks(ticksSize).tickFormat(d => tickYFormat(d)) d3.select("svg") .append("g") .attr("class", "y-axis") .attr("transform", "translate(" + margin.left + "," + margin.top + ")").call(yAxis); drawChart(selectedDimensionValue, selectedSliderIndex); function drawChart(selectedDimensionValue, selectedSliderIndex) { const selectedCategoryData = prepareChartData.resultData[selectedCategory]; const linesData = []; const selectedLineData = []; Object.keys(selectedCategoryData).forEach(seriesValue => { const seriesWiseData = []; selectedCategoryData[seriesValue].forEach((data, index) => { const dataObj = {}; dataObj["x"] = xScale(chartData.category[index]); dataObj["y"] = yScale(data); dataObj["value"] = data; dataObj["seriesValue"] = seriesValue; seriesWiseData.push(dataObj); }); if (seriesValue.toString().toLowerCase() == selectedDimensionValue) { selectedLineData.push({ dimension: seriesValue, data: seriesWiseData }); } linesData.push({ dimension: seriesValue, data: seriesWiseData }); }); d3.select(".line-container").remove(); const lineG = d3 .select("svg") .insert("g", '.y-axis') .attr("class", "line-container") .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); const curveType = "curveCatmullRom"; const curve = curveType && typeof d3[curveType] === "function" ? d3[curveType] : d3["curveLinear"]; const line = d3 .line() .defined(d => (typeof d == "object" ? d : !isNaN(d))) .curve(curve) .x(function (d, i) { return typeof d == "object" && d.x != null ? d.x : xScale(i); }) .y(function (d) { return typeof d == "object" && d.y != null ? d.y : yScale(d); }); d3.select(".line-container").html(""); ticksValues.forEach(dataTicks => { d3 .select(".line-container") .append("g") .attr("class", "line-zero-horizontal") .append("line") .attr("y1", yScale(dataTicks)) .attr("y2", yScale(dataTicks)) .attr("x1", xScale(chartData.category[0])) .attr("x2", xScale(chartData.category[chartData.category.length - 1])); }); d3 .select(".line-container") .append("g") .attr("class", "vertical-draggable-line") .append("line") .attr("y1", -20) .attr("y2", height - (margin.bottom + margin.top)) .attr("x1", xScale(selectedCategory)) .attr("x2", xScale(selectedCategory)); d3.select("svg").select(".y-axis") .attr("transform", `translate(${margin.left + xScale(selectedCategory)},${margin.top})`); //CBT:check y axis ticks exceed left border start let maxSize = -Infinity; d3.select("svg").select(".y-axis").selectAll('.tick').selectAll('text').nodes().forEach(node => { if (node.getBoundingClientRect().width > maxSize) { maxSize = node.getBoundingClientRect().width; } }); if (maxSize - xScale(selectedCategory) > 0) { d3.select("svg").select(".y-axis") .attr("transform", `translate(${margin.left + xScale(selectedCategory) + maxSize + 10},${margin.top})`); } //CBT:check y axis ticks exceed left border end const linePathG = d3 .select(".line-container") .append("g"); const className = "dimension"; const color = "#ccc"; linesData.forEach(dataOfLine => { linePathG .append("path") .datum(dataOfLine.data) .attr("class", `line ${className} ${"line_" + dataOfLine.dimension}`) .attr("data", JSON.stringify({ dimension: dataOfLine.dimension })) .attr("stroke", `${color ? color : "#000"}`) .attr("d", line) .on("click", (d, index) => { const data = JSON.parse(d3.event.target.getAttribute("data")); selectedDimensionValue = data.dimension.toString().toLowerCase(); drawChart(selectedDimensionValue, selectedSliderIndex); }); }); if (selectedLineData.length == 1) { linePathG .append("path") .datum(selectedLineData[0].data) .attr("class", `line ${className} selected`) .attr("d", line); const lastPoint = selectedLineData[0].data[selectedLineData[0].data.length - 1]; linePathG .append("circle") .attr("class", 'circle selected') .attr("cx", (lastPoint.x)) .attr("cy", (lastPoint.y)) .attr("r", 5); const textG = linePathG .append("g") .attr("transform", `translate(${lastPoint.x},${lastPoint.y})`); const text = textG.append("text").style("text-anchor", "end"); const title1 = text.append("tspan") .text(selectedDimensionValue) .attr("class", 'title1') .attr("x", 0); let lastValue = selectedLineData[0].data[selectedLineData[0].data.length - 1].value; lastValue = +parseFloat(lastValue).toFixed(0); lastValue = lastValue > 0 ? `${lastValue} percent more` : `${Math.abs(lastValue)} percent less`; const title2 = text.append("tspan") .attr("class", 'title2') .text(lastValue) .attr("x", 0); const title1Height = title1.node().getBoundingClientRect().height; title2.attr("y", (title1Height)); textG.node().setAttribute("transform", `translate(${lastPoint.x},${lastPoint.y - text.node().getBBox().height})`); const widthForLeftRect = xScale(selectedCategory); const widthForRightRect = (width - (margin.left + margin.right) - (widthForLeftRect + 2)); d3 .select(".line-container") .append("rect") .attr("class", "left-side-rect") .attr("width", widthForLeftRect) .attr("height", height - (margin.bottom + margin.top)); d3 .select(".line-container") .append("rect") .attr("class", "right-side-rect") .attr("x", widthForLeftRect + 2) .attr("width", widthForRightRect) .attr("height", height - (margin.bottom + margin.top)); var voronoiDiagram = d3 .voronoi() .x(function (d) { return d.x; }) .y(function (d) { return d.y; }) .size([widthForLeftRect, (height - (margin.top + margin.bottom))])([...linesData[0].data, ...linesData[1].data, ...linesData[2].data]); var voronoiDiagramRightRect = d3 .voronoi() .x(function (d) { return d.x; }) .y(function (d) { return d.y; }) .size([widthForRightRect, (height - (margin.top + margin.bottom))])([...linesData[0].data, ...linesData[1].data, ...linesData[2].data]); var voronoiRadius = widthForLeftRect; var voronoiRadiusRightRect = widthForLeftRect; d3.selectAll(".left-side-rect").on("mousemove", function (e) { var [mx, my] = d3.mouse(this); var site = voronoiDiagram.find(mx, my, voronoiRadius); var x = site[0]; var y = site[1]; d3.selectAll("." + className).classed('on-hover', false); let classes = d3.select(".line_" + site.data.seriesValue).attr("class"); d3.select(".line_" + site.data.seriesValue).attr("class", classes + " on-hover") }); d3.selectAll(".right-side-rect").on("mousemove", function (e) { var [mx, my] = d3.mouse(this); var site = voronoiDiagramRightRect.find(mx, my, voronoiRadiusRightRect); var x = site[0]; var y = site[1]; d3.selectAll("." + className).classed('on-hover', false); let classes = d3.select(".line_" + site.data.seriesValue).attr("class"); d3.select(".line_" + site.data.seriesValue).attr("class", classes + " on-hover") }); d3.selectAll(".left-side-rect") .on("click", function (e) { var [mx, my] = d3.mouse(this); var site = voronoiDiagram.find(mx, my, voronoiRadius); console.log("Data:", site.data.seriesValue); selectedDimensionValue = site.data.seriesValue.toString().toLowerCase(); drawChart(selectedDimensionValue, selectedSliderIndex); }); d3.selectAll(".right-side-rect") .on("click", function (e) { var [mx, my] = d3.mouse(this); var site = voronoiDiagramRightRect.find(mx, my, voronoiRadiusRightRect); console.log("Right Data:", site.data.seriesValue); selectedDimensionValue = site.data.seriesValue.toString().toLowerCase(); drawChart(selectedDimensionValue, selectedSliderIndex); }); d3 .selectAll(".left-side-rect") .on("mouseout", function (e) { d3.selectAll("." + className).classed('on-hover', false); }) d3 .selectAll(".right-side-rect") .on("mouseout", function (e) { d3.selectAll("." + className).classed('on-hover', false); }) } } $("#slider-range-max").slider({ range: "max", min: 0, max: chartData.category.length - 1, value: selectedSliderIndex, slide: function (event, ui) { console.log("Data", ui.value); d3.select(".left-rect-div").style("width", (parseFloat(xScale(chartData.category[ui.value]) + margin.left).toFixed(2) + "px")); selectedSliderIndex = ui.value; selectedCategory = chartData.category[selectedSliderIndex]; drawChart(selectedDimensionValue, selectedSliderIndex); } }); </script>