Я новичок в d3 js и после обучения у меня, когда я начал свой проект. Я хочу построить диаграмму, как показано на следующем рисунке:
![enter image description here](https://i.stack.imgur.com/DNsow.png)
Я бы хотел увеличить масштаб контекстного графа (маленький) применяется ко всем графам фокуса (большим) одновременно. Мой код следующий (dr aws вдохновлен https://observablehq.com/@connor-roche / multi-line-chart-focus-context ).
function generateRandomData(years){
var randomData = [];
labels = ['local', 'state', 'federal'];
for (var x in labels){
for (var i = 0; i < years; i++){
randomData.push({type: labels[x], year: i/100, taxRate: _getRandomArbitrary()});
}
}
return randomData;
// function _randomLocalData(){return Math.min(0.25, Math.random()/4);}
// function _randomStateData(){return Math.min(0.25, Math.random()/4);}
// function _randomFederalData(){return Math.min(0.25, Math.random()/4);}
function _randomLocalData(){return Math.random();}
function _randomStateData(){return Math.random();}
function _randomFederalData(){return Math.random();}
function _getRandomArbitrary() {
var min = -1000;
var max = 1000;
return Math.random() * (max - min) + min;
}
}
// ////// COMPOSE DATA
var taxData = generateRandomData(200, 100000);
// console.log(taxData);
var taxDataByYear = d3.nest()
.key(function(d){return d.year;})
.entries(taxData);
// console.log(taxDataByYear);
var taxDataByType = d3.nest()
.key(function(d){return d.type;})
.entries(taxData);
var taxDataForStack = taxDataByYear.reduce(function(taxData, data){
var newData = {year: data.key};
data.values.forEach(function(d){newData[d.type] = d.taxRate;});
taxData.push(newData);
return taxData;
}, []);
// console.log(taxDataForStack);
var years = taxDataByYear.map(function(d){return +d.key;});
// console.log(years);
var localTaxData = taxDataByType[0].values;
// console.log(localTaxData);
var stateTaxData = taxDataByType[1].values;
// console.log(stateTaxData);
var federalTaxData = taxDataByType[2].values;
// console.log(federalTaxData);
////// SETUP CONTAINERS
var focusChartMargin = {top: 40, right: 40, bottom: 100, left: 60};
width = 960 - focusChartMargin.left - focusChartMargin.right,
height = 500 - focusChartMargin.top - focusChartMargin.bottom,
contextChartMargin = { top: 430, right: focusChartMargin.right, bottom: 20, left: focusChartMargin.left };
// width of both charts
var chartWidth = width - focusChartMargin.left - focusChartMargin.right;
// height of either chart
var focusChartHeight = height - focusChartMargin.top - focusChartMargin.bottom;
// var contextChartHeight = height - contextChartMargin.top - contextChartMargin.bottom;
var contextChartHeight = 500 - contextChartMargin.top - contextChartMargin.bottom;
var svg = d3.select("body").append("svg")
.attr("width", width + focusChartMargin.left + focusChartMargin.right)
.attr("height", height + focusChartMargin.top + focusChartMargin.bottom);
var chartPadding = 15;
var chartHeight = height / taxDataByType.length - 30;
// var chartHeight = 90;
// console.log('focusChartMargin.top: ', focusChartMargin.top);
// console.log('focusChartMargin.right: ', focusChartMargin.right);
// console.log('focusChartMargin.bottom: ', focusChartMargin.bottom);
// console.log('focusChartMargin.left: ', focusChartMargin.left);
// console.log('height:', height);
// console.log('width:', width);
// console.log('data len:', taxDataByType.length);
// console.log('h2;', contextChartHeight);
// console.log('contextChartMargin.top: ', contextChartMargin.top);
// console.log('contextChartMargin.right: ', contextChartMargin.right);
// console.log('contextChartMargin.bottom: ', contextChartMargin.bottom);
// console.log('contextChartMargin.left: ', contextChartMargin.left);
// console.log('chartHeight: ', focusChartHeight);
////// COMPOSE GENERATORS
var generators = {
'shared': {
color: d3.scaleOrdinal(d3.schemeAccent)
.domain(['local', 'state', 'federal']),
stack: d3.stack()
.keys(['local', 'state', 'federal'])
.order(d3.stackOrderDescending)
.offset(d3.stackOffsetNone),
xContext: d3.scaleLinear()
.domain([d3.min(years), d3.max(years)])
.range([0, width]),
xFocus: d3.scaleLinear()
.domain([d3.min(years), d3.max(years)])
.range([0, width]),
yFocus: d3.scaleLinear()
.domain([d3.max(federalTaxData, function(d){return d.taxRate;}), d3.min(federalTaxData, function(d){return d.taxRate;})])
.range([0, height]),
yContext: d3.scaleLinear()
.domain([d3.max(federalTaxData, function(d){return d.taxRate;}), d3.min(federalTaxData, function(d){return d.taxRate;})])
.range([0, contextChartHeight]),
},
'local': {},
'state': {},
'federal': { yFocus: d3.scaleLinear()
.domain([d3.max(federalTaxData, function(d){return d.taxRate;}), d3.min(federalTaxData, function(d){return d.taxRate;})])
.range([0, chartHeight])}
};
var stackTaxData = generators.shared.stack(taxDataForStack);
var maxStackData = d3.max(stackTaxData, function(d){return d3.max(d, function(d){return d[1];});});
generators.shared.xAxisFocus = d3.axisBottom(generators.shared.xFocus)
.tickFormat(d3.format('.2r'))
.tickSizeInner(-height);
generators.shared.xAxisContex = d3.axisBottom(generators.shared.xContext)
.tickFormat(d3.format('.2r'))
.tickSizeInner(0);
generators.shared.yAxisContex = d3.axisLeft(generators.shared.yContext)
.tickFormat(d3.format('.0f'))
.tickSize(1);
generators.federal.yAxis = d3.axisLeft(generators.federal.yFocus)
.tickFormat(d3.format('.0f'))
.tickSize(1)
.tickSizeInner(-width);
generators.shared.yAxis = d3.axisLeft(generators.shared.yFocus)
.tickFormat(d3.format('.2f'))
.tickSize(1);
generators.shared.area = d3.line()
.x(function(d){return generators.shared.xFocus(d.year);})
.y(function(d){return generators.federal.yFocus(d.taxRate);})
.curve(d3.curveMonotoneX);
var lineContext = d3
.line()
.x((d) => generators.shared.xContext(d.year))
.y(d => generators.shared.yContext(d.taxRate));
generators.local.area = d3.line(localTaxData)
.x(function(d){return generators.shared.xFocus(d.year);})
.y(function(d){return generators.federal.yFocus(d.taxRate);})
.curve(d3.curveMonotoneX);
generators.state.area = d3.line(stateTaxData)
.x(function(d){return generators.shared.xFocus(d.year);})
.y(function(d){return generators.federal.yFocus(d.taxRate);})
.curve(d3.curveMonotoneX);
generators.federal.area = d3.line(federalTaxData)
.x(function(d){return generators.shared.xFocus(d.year);})
.y(function(d){return generators.federal.yFocus(d.taxRate);})
.curve(d3.curveMonotoneX);
// build brush
var brush = d3.brushX()
.extent([[0, -10],[width, contextChartHeight],])
.on("brush end", brushed);
// build zoom for the focus chart
// as specified in "filter" - zooming in/out can be done by pinching on the trackpad while mouse is over focus chart
// zooming in can also be done by double clicking while mouse is over focus chart
var zoom = d3.zoom()
.scaleExtent([1, Infinity])
.translateExtent([[0, 0],[chartWidth, focusChartHeight],])
.extent([[0, 0],[chartWidth, focusChartHeight],])
.on("zoom", zoomed)
.filter(() => d3.event.ctrlKey || d3.event.type === "dblclick" || d3.event.type === "mousedown");
// clip is created so when the focus chart is zoomed in the data lines don't extend past the borders
var clip = svg
.append("defs")
.append("svg:clipPath")
.attr("id", "clip")
.append("svg:rect")
.attr("width", width )
.attr("height", height)
.attr("x", 0)
.attr("y", 0);
// append the clip
var focusChartLines = svg
.append("g")
.attr("class", "focus")
.attr("transform", "translate(" + focusChartMargin.left + "," + focusChartMargin.top + ")")
.attr("clip-path", "url(#clip)");
// create focus chart
var focus = svg
.append("g")
.attr("class", "focus")
.attr("transform", "translate(" + focusChartMargin.left + "," + focusChartMargin.top + ")");
var context = svg
.append("g")
.attr("class", "context")
.attr("transform", "translate(" + contextChartMargin.left + "," + (contextChartMargin.top) + ")");
// xFocus.domain(d3.extent(dates));
// yFocus.domain([0, maxYAxisValue]);
// xContext.domain(d3.extent(dates));
// yContext.domain(yFocus.domain());
focus.append('g')
.attr('class','x-axis')
.attr('transform', 'translate(0,' + (height - 55) + ')')
.call(generators.shared.xAxisFocus);
focus.selectAll('.eachFocus').data(taxDataByType).enter()
.append('g')
.attr('transform', function(d, i){ return 'translate(0, ' + ((chartHeight + chartPadding) * i) + ')'; })
.attr('class', 'eachFocus')
.call(generators.federal.yAxis);
var allFocus = focusChartLines.selectAll('.eachFocus').data(taxDataByType).enter()
.append('g')
.attr('transform', function(d, i){ return 'translate(0, ' + ((chartHeight + chartPadding) * i) + ')'; })
.attr('class', 'eachFocus')
.call(generators.federal.yAxis);
allFocus.append('path')
.attr('d', function(d){return generators[d.key].area(d.values);})
.attr('class', 'stack-container')
.attr('stroke', '#1c1f1d')
.attr("stroke-width", "1")
.attr("fill", "none");
context
.append("g")
.attr("class", "x-axis")
.attr("transform", "translate(0," + contextChartHeight + ")")
.call(generators.shared.xAxisContex);
// go through data and create/append lines to both charts
for (let key of [0, 1, 2]) {
let bucket = taxDataByType[key].values;
var label = taxDataByType[key].key;
context
.append("path")
.datum(bucket)
.attr("class", "line")
.attr("fill", "none")
.attr("stroke", "black")
.attr('stroke', (d)=>{return generators.shared.color(label);})
.attr("stroke-width", 1.5)
.attr("d", lineContext);
}
var contextBrush = context
.append("g")
.attr("class", "brush")
.call(brush);
var brushHandlePath = d => {
var e = +(d.type === "e"),
x = e ? 1 : -1,
y = contextChartHeight + 10 ;
return (
"M" + 0.5 * x + "," + y +"A6,6 0 0 " +e +" " +6.5 * x +"," +(y + 6) +"V" +(2 * y - 6) +"A6,6 0 0 " +e +" " +0.5 * x +"," +2 * y +
"Z" +"M" +2.5 * x +"," +(y + 8) +"V" +(2 * y - 8) +"M" +4.5 * x +"," +(y + 8) +"V" +(2 * y - 8)
);
};
var brushHandle = contextBrush
.selectAll(".handle--custom")
.data([{ type: "w" }, { type: "e" }])
.enter()
.append("path")
.attr("class", "handle--custom")
.attr("stroke", "#000")
.attr("cursor", "ew-resize")
.attr("d", brushHandlePath);
svg
.append("rect")
.attr("cursor", "move")
.attr("fill", "none")
.attr("pointer-events", "all")
.attr("class", "zoom")
.attr("width", width)
.attr("height", height)
.attr("transform", "translate(" + focusChartMargin.left + "," + focusChartMargin.top + ")")
.call(zoom);
contextBrush.call(brush.move, [0, width / 2]);
// functions
function brushed() {
console.log("brush");
var xContext = generators.shared.xContext;
var xAxisFocus = generators.shared.xAxisFocus;
if (d3.event.sourceEvent && d3.event.sourceEvent.type === "zoom") return; // ignore brush-by-zoom
var s = d3.event.selection || xContext.range();
xContext.domain(s.map(xContext.invert, xContext));
allFocus.selectAll(".eachFocus").attr("d", generators.local.area);
allFocus.select(".x-axis").call(xAxisFocus);
svg.select(".zoom").call(zoom.transform, d3.zoomIdentity.scale(width / (s[1] - s[0])).translate(-s[0], 0));
brushHandle
.attr("display", null)
.attr("transform", (d, i) => "translate(" + [s[i], -contextChartHeight - 20] + ")");
}
function zoomed() {
console.log("zoomed");
if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") return; // ignore zoom-by-brush
var t = d3.event.transform;
var xContext = generators.shared.xContext;
var xAxisFocus = generators.shared.xAxisFocus;
var xFocus= generators.shared.xFocus;
xFocus.domain(t.rescaleX(xContext).domain());
allFocus.selectAll(".line").attr("d", generators.local.area);
allFocus.select(".x-axis").call(xAxisFocus);
var brushSelection = xFocus.range().map(t.invertX, t);
context.select(".brush").call(brush.move, brushSelection);
brushHandle
.attr("display", null)
.attr("transform", (d, i) => "translate(" + [brushSelection[i], -contextChartHeight - 20] + ")");
}
Хотя отображаются все графики, никакого взаимодействия не происходит. Я думаю, это потому, что я еще не понимаю, как работают bru sh и масштабирование. Несмотря на то, что я много читаю об этом топи c, я все еще не могу с ним справиться. Может ли кто-нибудь обратиться ко мне по правильному пути? Заранее спасибо.