我有一個圖表,它為每個 x 軸專案使用設定的寬度,因此當添加大量日期時,圖表是可滾動的。
我想要做的是讓兩個 Y 軸標簽始終在螢屏上(我不能position: fixed使用 CSS,因為軸是<g>元素。)
如何讓兩個 Y 軸標簽始終顯示在螢屏上?(這是藍色和綠色文本標簽)。這是我的意思的一個例子:https : //observablehq.com/@d3/pannable-chart ) - 不幸的是,該網站上的代碼在我頭上編碼。
let test = (async () => {
// data source
const data = JSON.parse(`{
"2021-11-17":{
"rawWeight":220,
"rca":1821
},
"2021-05-17":{
"rawWeight":230,
"rca":1600
},
"2021-03-09":{
"rawWeight":224,
"rca":1800
},
"2020-10-30":{
"rawWeight":234.36,
"rca":2851
},
"2020-10-13":{
"rawWeight":225.54,
"rca":2541
},
"2020-09-25":{
"rawWeight":225.4,
"rca":2588
},
"2020-1-10":{
"rawWeight":244,
"rca":1800
}
}`)
// parse the date / time
var parseTime = d3.timeParse("%Y-%m-%d");
//structure dataset
let dataset = []
for (let day in data) {
dataset.push({
day: day,
date: parseTime(day),
weight: Number(data[day].rawWeight),
calories: data[day].rca
})
}
let margin = {
top: 10,
right: 20,
bottom: 0,
left: 20
}
//let width = document.querySelector('.pane[data-area="weight"] .chart').clientWidth - margin.left - margin.right
let width = (dataset.length * 230) - margin.left - margin.right
let height = 150 - margin.top - margin.bottom
// set the ranges
let x = d3.scaleTime().range([0, width])
let y0 = d3.scaleLinear().range([height, 0])
let y1 = d3.scaleLinear().range([height, 0])
let linecalories = d3.line()
.curve(d3.curveCatmullRom)
.x(d => x(d.date))
.y(d => y0(d.calories))
let areacalories = d3.area()
.curve(d3.curveCatmullRom)
.x(d => x(d.date))
.y0(height)
.y1(d => y0(d.calories))
let lineweight = d3.line()
.curve(d3.curveCatmullRom)
.x(d => x(d.date))
.y(d => y1(d.weight))
let areaweight = d3.area()
.curve(d3.curveCatmullRom)
.x(d => x(d.date))
.y0(height)
.y1(d => y1(d.weight))
let svg = d3
.select('.pane[data-area="weight"] .chart')
.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 ")")
// Scale the range of the data
x.domain(d3.extent(dataset, d => d.date))
y0.domain([0, d3.max(dataset, (d) => {
// let rounded = Math.floor( Math.max(d.calories) / 500) * 500
// return rounded 1000
return Math.max(d.calories) 500
})])
y1.domain([
// replace this with "0" to show scale from 0
d3.min(dataset, d => Math.min(d.weight) - 25),
d3.max(dataset, d => Math.max(d.weight) 25)
])
// gridlines in y axis function
function make_y_gridlines() {
return d3.axisLeft(y1)
.ticks(8)
}
svg.append("g")
.attr("class", "grid-y")
.call(make_y_gridlines()
.tickSize(-width)
.ticks(5)
)
.call(g => g.select(".domain").remove())
.call(g => g.selectAll("text").remove())
// gridlines in x axis function
function make_x_gridlines() {
return d3.axisBottom(x)
.ticks(20)
}
svg.append("g")
.attr("class", "grid-x")
.attr("transform", "translate(0," height ")")
.call(make_x_gridlines()
.tickSize(-height)
)
.call(g => g.select(".domain").remove())
.call(g => g.selectAll("text").remove())
// apply weight area
svg.append("path")
.data([dataset])
.attr("class", "area-weight")
.attr("d", areaweight)
// apply calories area
svg.append("path")
.data([dataset])
.attr("class", "area-calories")
.attr("d", areacalories)
// apply weight line
svg.append("path")
.data([dataset])
.attr("class", "line-weight")
.attr("d", lineweight)
// apply calories line
svg.append("path")
.data([dataset])
.attr("class", "line-calories")
.attr("d", linecalories)
svg.append("g")
.attr("class", "axis-dates")
.attr("transform", "translate(0," (height 8) ")")
.call(
d3
.axisBottom(x)
.tickSize(0)
.ticks(d3.utcMonth.every(1))
.tickSizeOuter(0)
.tickFormat(d3.timeFormat("%b %Y"))
.tickPadding(-30)
)
.call(g => g.select(".domain").remove())
// Add the Y0 Axis
svg.append("g")
.attr("class", "axis-calories")
.attr("transform", "translate( " width ", 0 )")
.call(
d3
.axisRight()
.scale(y0)
.tickSize(0)
.ticks(height / 30)
.tickFormat(d => {
if ((d / 1000) >= 1)
d = d / 1000 "K";
return d
})
)
.call(g => g.select(".domain").remove())
// Add the Y1 Axis
svg.append("g")
.attr("class", "axis-weight")
// .attr("transform", "translate( " width ", 0 )")
.call(
d3
.axisLeft()
.scale(y1)
.tickSize(0)
.ticks(height / 30)
)
.call(g => g.select(".domain").remove())
// dates
// svg.append("g")
// .attr("class", "axis-dates")
// .attr("transform", "translate(0," (height 8) ")")
// .call(
// d3
// .axisBottom(x)
// .ticks(0)
// .tickValues(x.domain())
// .tickFormat(d3.timeFormat("%b %Y"))
// .tickPadding(-30)
// )
// .call(g => g.select(".domain").remove())
// .call(g => g.selectAll("line").remove())
// .call(g => g.select(".tick:first-of-type text").attr('transform', 'translate(32,0)'))
// .call(g => g.select(".tick:last-of-type text").attr('transform', 'translate(-32,0)'))
// remove 0 label
svg.selectAll(".tick text")
.filter(function(d) {
return d === 0
})
.remove()
let colors = ['#56ab2f', '#a8e063']
let grad = svg.append('defs')
.append('linearGradient')
.attr('id', 'calorieline')
.attr('x1', '0%')
.attr('x2', '100%')
.attr('y1', '0%')
.attr('y2', '100%');
grad.selectAll('stop')
.data(colors)
.enter()
.append('stop')
.style('stop-color', function(d) {
return d;
})
.attr('offset', function(d, i) {
return 100 * (i / (colors.length - 1)) '%';
})
colors = ['rgba(86, 171, 47, .15)', 'transparent']
grad = svg.append('defs')
.append('linearGradient')
.attr('id', 'greenfade')
.attr('x1', '0%')
.attr('x2', '0%')
.attr('y1', '0%')
.attr('y2', '100%');
grad.selectAll('stop')
.data(colors)
.enter()
.append('stop')
.style('stop-color', function(d) {
return d;
})
.attr('offset', function(d, i) {
return 100 * (i / (colors.length - 1)) '%';
})
colors = ['rgba(32, 120, 227, .15)', 'transparent']
grad = svg.append('defs')
.append('linearGradient')
.attr('id', 'bluefade')
.attr('x1', '0%')
.attr('x2', '0%')
.attr('y1', '0%')
.attr('y2', '100%');
grad.selectAll('stop')
.data(colors)
.enter()
.append('stop')
.style('stop-color', function(d) {
return d;
})
.attr('offset', function(d, i) {
return 100 * (i / (colors.length - 1)) '%';
})
colors = ['#2894f2', '#1d6cdc']
grad = svg.append('defs')
.append('linearGradient')
.attr('id', 'goodbar')
.attr('x1', '0%')
.attr('x2', '25%')
.attr('y1', '0%')
.attr('y2', '100%');
grad.selectAll('stop')
.data(colors)
.enter()
.append('stop')
.style('stop-color', function(d) {
return d;
})
.attr('offset', function(d, i) {
return 100 * (i / (colors.length - 1)) '%';
})
});
test();
body {
background: #1e2546;
width: 500px;
display: block;
}
.chart .axis-dates text {
font-size: .7rem;
fill: #fff;
}
[data-area=weight] .chart .axis-dates .tick {
margin-left: -100px;
left: 10rem;
position: relative
}
[data-area=weight] .chart .line-calories {
stroke-linecap: round;
stroke-width: .2rem;
stroke: url(#calorieline);
fill: none
}
[data-area=weight] .chart .area-calories {
fill: url(#greenfade);
}
[data-area=weight] .chart .area-weight {
fill: url(#bluefade)
}
[data-area=weight] .chart .axis-calories text {
fill: url(#calorieline);
font-size: .6rem;
font-weight: 500
}
[data-area=weight] .chart .line-weight {
stroke-linecap: round;
stroke-width: .2rem;
stroke: url(#goodbar);
fill: transparent
}
[data-area=weight] .chart .axis-weight text {
font-size: .6rem;
font-weight: 500;
fill: url(#goodbar);
}
[data-area=weight] .chart .grid-x line,
[data-area=weight] .chart .grid-y line {
stroke: rgba(0, 0, 0, .1);
stroke-width: .1rem
}
[data-area=weight] .chart .label-calorie {
font-size: .7rem
}
[data-area=weight] .chart .legend {
-webkit-column-count: 2;
-moz-column-count: 2;
column-count: 2;
-webkit-column-gap: .5rem;
-moz-column-gap: .5rem;
column-gap: .5rem;
margin: .3rem auto 0 auto;
width: 9rem;
font-size: .9rem;
text-align: center;
position: relative
}
[data-area=weight] .chart .legend .i {
display: block;
vertical-align: top;
width: 100%
}
[data-area=weight] .chart .legend .i:before {
content: '';
display: inline-block;
width: .6rem;
height: .3rem;
border-radius: 5rem;
margin-right: .3rem;
vertical-align: .1rem
}
[data-area=weight] .chart .legend .calorie {
color: #a7e063
}
[data-area=weight] .chart .legend .calorie:before {
background: #a7e063
}
[data-area=weight] .chart .legend .weight {
color: #2482e9
}
[data-area=weight] .chart .legend .weight:before {
background: #2482e9
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.4/d3.min.js" integrity="sha512-T 1zstV6Llwh/zH uoc1rJ7Y8tf9N DiC0T3aL0 0blupn5NkBT52Avsa0l XBnftn/14EtxpsztAWsmiAaqfQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
</head>
<body>
<div class="pane" data-area="weight">
<div class="chart"></div>
</div>
</body>
</html>
這是一個 JSFiddle:https ://jsfiddle.net/8h5fxn46/
uj5u.com熱心網友回復:
我將在回答前說通常不建議在同一圖表上使用兩個 y 軸。同樣,如果您一次只顯示折線圖的一部分并強迫讀者滾動查看圖表的其余部分,那么他們將更難進行比較并確定整個資料集的趨勢。這樣做需要他們記住當前未顯示的資料是什么樣的。使用畫筆和縮放或縮放可能會更好,因為讀者既可以獲得整個折線圖的概覽,也可以關注其中的特定部分。
話雖如此,以下是基于您鏈接到的 Observable 示例,您可以如何在具有兩個 y 軸的圖表上滾動。基本思想是將 y 軸放在一個 SVG 元素中。然后圖表的其余部分將進入另一個 SVG 元素,放置在一個 div 中。div 將處理滾動。我們將兩個 SVG 疊加在一起。
<!-- references https://observablehq.com/@d3/pannable-chart -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://d3js.org/d3.v7.js"></script>
</head>
<body>
<div id="chart"></div>
<script>
// data prepartion
const rawData = {
"2021-11-17": { rawWeight: 220, rca: 1821 },
"2021-05-17": { rawWeight: 230, rca: 1600 },
"2021-03-09": { rawWeight: 224, rca: 1800 },
"2020-10-30": { rawWeight: 234.36, rca: 2851 },
"2020-10-13": { rawWeight: 225.54, rca: 2541 },
"2020-09-25": { rawWeight: 225.4, rca: 2588 },
"2020-1-10": { rawWeight: 244, rca: 1800 },
};
const parseTime = d3.timeParse("%Y-%m-%d");
const dataset = Object.entries(rawData).map(([date, { rawWeight, rca }]) => ({
date: parseTime(date),
weight: rawWeight,
calories: rca,
}));
// set up
const margin = { top: 30, bottom: 30, left: 30, right: 30 };
const viewableWidth = 600;
const totalWidth = dataset.length * 230;
const height = 200;
const parent = d3.select('#chart');
const yAxesSvg = parent.append('svg')
.attr('width', viewableWidth)
.attr('height', height)
.style('position', 'absolute')
.style('pointer-events', 'none')
.style('z-index', 1);
const body = parent.append('div')
.style('overflow-x', 'scroll')
.style('max-width', `${viewableWidth}px`)
.style('-webkit-overflow-scrolling', 'touch');
const mainSvg = body.append('svg')
.attr('width', totalWidth)
.attr('height', height)
.style('display', 'block');
// scales
const x = d3.scaleTime()
.domain(d3.extent(dataset, d => d.date))
.range([margin.left, totalWidth - margin.right]);
const yWeight = d3.scaleLinear()
.domain([0, d3.max(dataset, d => d.weight)])
.range([height - margin.bottom, margin.top]);
const yCalories = d3.scaleLinear()
.domain([0, d3.max(dataset, d => d.calories)])
.range([height - margin.bottom, margin.top]);
// line generators
const weightLine = d3.line()
.x(d => x(d.date))
.y(d => yWeight(d.weight));
const caloriesLine = d3.line()
.x(d => x(d.date))
.y(d => yCalories(d.calories));
// axes
// x axis
mainSvg.append('g')
.attr('transform', `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x)
.tickSize(0)
.ticks(d3.timeMonth.every(1))
.tickSizeOuter(0)
.tickFormat(d3.timeFormat("%b %Y")))
.call(g => g.select(".domain").remove());
// weight axis
yAxesSvg.append('g')
.attr('transform', `translate(${margin.left},0)`)
// add white background rectangle so that the lines won't overlap the axis
.call(g => g.append('rect')
.attr('fill', 'white')
.attr('width', margin.left)
.attr('x', -margin.left)
.attr('y', 0)
.attr('height', height))
.call(d3.axisLeft(yWeight)
.tickSize(0)
.ticks(height / 30))
.call(g => g.select(".domain").remove())
// change color of tick labels
.call(g => g.selectAll('.tick > text').attr('fill', 'blue'))
// add axis label
.call(g => g.append('text')
.attr('fill', 'blue')
.attr('text-anchor', 'start')
.attr('dominant-baseline', 'hanging')
.attr('font-weight', 'bold')
.attr('y', 0)
.attr('x', -margin.left)
.text('Weight'));
// calories axis
yAxesSvg.append("g")
.attr("transform", `translate(${viewableWidth - margin.right},0)`)
// add white background rectangle so that the lines won't overlap the axis
.call(g => g.append('rect')
.attr('fill', 'white')
.attr('x', 0)
.attr('width', margin.right)
.attr('y', 0)
.attr('height', height))
.call(d3.axisRight(yCalories)
.tickSize(0)
.ticks(height / 30, '~s'))
// change color of tick labels
.call(g => g.selectAll('.tick > text').attr('fill', 'green'))
.call(g => g.select(".domain").remove())
// add axis label
.call(g => g.append('text')
.attr('fill', 'green')
.attr('text-anchor', 'end')
.attr('dominant-baseline', 'hanging')
.attr('font-weight', 'bold')
.attr('y', 0)
.attr('x', margin.right)
.text('Calories'));
// lines
mainSvg.append('g')
.datum(dataset)
.append('path')
.attr('stroke', 'blue')
.attr('fill', 'none')
.attr('d', weightLine);
mainSvg.append('g')
.datum(dataset)
.append('path')
.attr('stroke', 'green')
.attr('fill', 'none')
.attr('d', caloriesLine);
</script>
</body>
</html>
轉載請註明出處,本文鏈接:https://www.uj5u.com/gongcheng/326037.html
標籤:javascript d3.js
