我在把这个可观察的例子转换成jsfiddle时遇到了麻烦。
https://observablehq.com/@d3 focus-context ?收集= @d3/d3-brush
这是我的护照https://jsfiddle.net/u5g1ychz/1/
编辑:最新的小提琴尝试https://jsfiddle.net/03eagvxk/
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
</head>
<body>
<h1>My Chart</h1>
<div id="my_chart"></div>
</body>
</html>
CSS
.line {
fill: none;
stroke: #ffab00;
stroke-width: 1.5;
}
.overlay {
fill: none;
pointer-events: all;
}
/* Style the dots by assigning a fill and stroke */
.dot {
fill: #ffab00;
stroke: #fff;
}
.focus circle {
fill: none;
stroke: steelblue;
}
Javascript
/* new data */
var x = d3.timeDays(new Date(2010, 06, 01), new Date(2020, 10, 30));
var y = Array.from({length: x.length}, Math.random).map(n => Math.floor(n * 10) + 5);
var data = x.map((v, i) => {
return {
"date": v,
"close": y[i]
}
});
/* begin observable code */
viewof focus = {
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, focusHeight])
.style("display", "block");
const brush = d3.brushX()
.extent([[margin.left, 0.5], [width - margin.right, focusHeight - margin.bottom + 0.5]])
.on("brush", brushed)
.on("end", brushended);
const defaultSelection = [x(d3.utcYear.offset(x.domain()[1], -1)), x.range()[1]];
svg.append("g")
.call(xAxis, x, focusHeight);
svg.append("path")
.datum(data)
.attr("fill", "steelblue")
.attr("d", area(x, y.copy().range([focusHeight - margin.bottom, 4])));
const gb = svg.append("g")
.call(brush)
.call(brush.move, defaultSelection);
function brushed({selection}) {
if (selection) {
svg.property("value", selection.map(x.invert, x).map(d3.utcDay.round));
svg.dispatch("input");
}
}
function brushended({selection}) {
if (!selection) {
gb.call(brush.move, defaultSelection);
}
}
return svg.node();
}
update = {
const [minX, maxX] = focus;
const maxY = d3.max(data, d => minX <= d.date && d.date <= maxX ? d.value : NaN);
chart.update(x.copy().domain(focus), y.copy().domain([0, maxY]));
}
area = (x, y) => d3.area()
.defined(d => !isNaN(d.value))
.x(d => x(d.date))
.y0(y(0))
.y1(d => y(d.value))
x = d3.scaleUtc()
.domain(d3.extent(data, d => d.date))
.range([margin.left, width - margin.right])
y = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)])
.range([height - margin.bottom, margin.top])
xAxis = (g, x, height) => g
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x).ticks(width / 80).tickSizeOuter(0))
yAxis = (g, y, title) => g
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y))
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".title").data([title]).join("text")
.attr("class", "title")
.attr("x", -margin.left)
.attr("y", 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text(title))
margin = ({top: 20, right: 20, bottom: 30, left: 40})
height = 440
focusHeight = 100
下载的observable javascript文件
// https://observablehq.com/@d3/focus-context@326
export default function define(runtime, observer) {
const main = runtime.module();
const fileAttachments = new Map([["aapl.csv",new URL("./files/de259092d525c13bd10926eaf7add45b15f2771a8b39bc541a5bba1e0206add4880eb1d876be8df469328a85243b7d813a91feb8cc4966de582dc02e5f8609b7",import.meta.url)]]);
main.builtin("FileAttachment", runtime.fileAttachments(name => fileAttachments.get(name)));
main.variable(observer()).define(["md"], function(md){return(
md`# Focus + Context
This [area chart](/@d3/area-chart) uses brushing to specify a focused area. Drag the gray region to pan, or brush to zoom. Compare to a [zoomable chart](/@d3/zoomable-area-chart). Data: [Yahoo Finance](https://finance.yahoo.com/lookup)`
)});
main.variable(observer("chart")).define("chart", ["d3","width","height","DOM","margin","data","xAxis","yAxis","area"], function(d3,width,height,DOM,margin,data,xAxis,yAxis,area)
{
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.style("display", "block");
const clipId = DOM.uid("clip");
svg.append("clipPath")
.attr("id", clipId.id)
.append("rect")
.attr("x", margin.left)
.attr("y", 0)
.attr("height", height)
.attr("width", width - margin.left - margin.right);
const gx = svg.append("g");
const gy = svg.append("g");
const path = svg.append("path")
.datum(data)
.attr("clip-path", clipId)
.attr("fill", "steelblue");
return Object.assign(svg.node(), {
update(focusX, focusY) {
gx.call(xAxis, focusX, height);
gy.call(yAxis, focusY, data.y);
path.attr("d", area(focusX, focusY));
}
});
}
);
main.variable(observer("viewof focus")).define("viewof focus", ["d3","width","focusHeight","margin","x","xAxis","data","area","y"], function(d3,width,focusHeight,margin,x,xAxis,data,area,y)
{
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, focusHeight])
.style("display", "block");
const brush = d3.brushX()
.extent([[margin.left, 0.5], [width - margin.right, focusHeight - margin.bottom + 0.5]])
.on("brush", brushed)
.on("end", brushended);
const defaultSelection = [x(d3.utcYear.offset(x.domain()[1], -1)), x.range()[1]];
svg.append("g")
.call(xAxis, x, focusHeight);
svg.append("path")
.datum(data)
.attr("fill", "steelblue")
.attr("d", area(x, y.copy().range([focusHeight - margin.bottom, 4])));
const gb = svg.append("g")
.call(brush)
.call(brush.move, defaultSelection);
function brushed({selection}) {
if (selection) {
svg.property("value", selection.map(x.invert, x).map(d3.utcDay.round));
svg.dispatch("input");
}
}
function brushended({selection}) {
if (!selection) {
gb.call(brush.move, defaultSelection);
}
}
return svg.node();
}
);
main.variable(observer("focus")).define("focus", ["Generators", "viewof focus"], (G, _) => G.input(_));
main.variable(observer("update")).define("update", ["focus","d3","data","chart","x","y"], function(focus,d3,data,chart,x,y)
{
const [minX, maxX] = focus;
const maxY = d3.max(data, d => minX <= d.date && d.date <= maxX ? d.value : NaN);
chart.update(x.copy().domain(focus), y.copy().domain([0, maxY]));
}
);
main.variable(observer("data")).define("data", ["d3","FileAttachment"], async function(d3,FileAttachment){return(
Object.assign(d3.csvParse(await FileAttachment("aapl.csv").text(), d3.autoType).map(({date, close}) => ({date, value: close})), {y: "↑ Close $"})
)});
main.variable(observer("area")).define("area", ["d3"], function(d3){return(
(x, y) => d3.area()
.defined(d => !isNaN(d.value))
.x(d => x(d.date))
.y0(y(0))
.y1(d => y(d.value))
)});
main.variable(observer("x")).define("x", ["d3","data","margin","width"], function(d3,data,margin,width){return(
d3.scaleUtc()
.domain(d3.extent(data, d => d.date))
.range([margin.left, width - margin.right])
)});
main.variable(observer("y")).define("y", ["d3","data","height","margin"], function(d3,data,height,margin){return(
d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)])
.range([height - margin.bottom, margin.top])
)});
main.variable(observer("xAxis")).define("xAxis", ["margin","d3","width"], function(margin,d3,width){return(
(g, x, height) => g
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x).ticks(width / 80).tickSizeOuter(0))
)});
main.variable(observer("yAxis")).define("yAxis", ["margin","d3"], function(margin,d3){return(
(g, y, title) => g
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y))
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".title").data([title]).join("text")
.attr("class", "title")
.attr("x", -margin.left)
.attr("y", 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text(title))
)});
main.variable(observer("margin")).define("margin", function(){return(
{top: 20, right: 20, bottom: 30, left: 40}
)});
main.variable(observer("height")).define("height", function(){return(
440
)});
main.variable(observer("focusHeight")).define("focusHeight", function(){return(
100
)});
main.variable(observer("d3")).define("d3", ["require"], function(require){return(
require("d3@6")
)});
return main;
}
这很棘手,因为函数是可观察的,而视图返回的是画笔的视图(最小值和最大值)
这是一个工作小提琴
/* new data */
var x = d3.timeDays(new Date(2015, 06, 01), new Date(2020, 10, 30));
var y = Array.from({length: x.length}, Math.random).map(n => Math.floor(n * 10) + 5);
var data = x.map((v, i) => {
return {
"date": v,
"value": y[i]
}
});
var margin = {top: 20, right: 20, bottom: 30, left: 40}
var height = 440;
var width = 600;
var focusHeight = 100;
var focusedArea = d3.extent(x);
const svg = d3.select('#my_chart').append("svg")
.attr("viewBox", [0, 0, width, height])
.style("display", "block");
const clipId = {id: "clip"};
const clip = svg.append("clipPath")
.attr("id", clipId.id)
.append("rect")
.attr("x", margin.left)
.attr("y", 0)
.attr("height", height)
.attr("width", width - margin.left - margin.right);
const gx = svg.append("g");
const gy = svg.append("g");
const path = svg.append("path")
.datum(data)
.attr("clip-path", `url(#${clipId.id})`)
.attr("fill", "steelblue");
const updateChart = (focusX, focusY) => {
gx.call(xAxis, focusX, height);
gy.call(yAxis, focusY, data.y);
path.attr("d", area(focusX, focusY));
};
/* begin observable code */
var focus = () => {
const svg = d3.select("#focus").append("svg")
.attr("viewBox", [0, 0, width, focusHeight])
.style("display", "block");
const brush = d3.brushX()
.extent([[margin.left, 0.5], [width - margin.right, focusHeight - margin.bottom + 0.5]])
.on("brush", brushed)
.on("end", brushended);
const defaultSelection = [x(d3.utcYear.offset(x.domain()[1], -1)), x.range()[1]];
svg.append("g")
.call(xAxis, x, focusHeight);
svg.append("path")
.datum(data)
.attr("fill", "steelblue")
.attr("d", area(x, y.copy().range([focusHeight - margin.bottom, 4])));
const gb = svg.append("g")
.call(brush)
.call(brush.move, defaultSelection);
function brushed({selection}) {
if (selection) {
svg.property("value", selection.map(x.invert, x).map(d3.utcDay.round));
svg.dispatch("input");
focusedArea = svg.property('value');
update();
}
}
function brushended({selection}) {
if (!selection) {
gb.call(brush.move, defaultSelection);
}
}
return svg.node();
}
var update = function() {
const [minX, maxX] = focusedArea;
const maxY = d3.max(data, d => minX <= d.date && d.date <= maxX ? d.value : NaN);
updateChart(x.copy().domain(focusedArea), y.copy().domain([0, maxY]));
}
var area = (x, y) => d3.area()
.defined(d => !isNaN(d.value))
.x(d => x(d.date))
.y0(y(0))
.y1(d => y(d.value))
var x = d3.scaleUtc()
.domain(d3.extent(data, d => d.date))
.range([margin.left, width - margin.right])
var y = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)])
.range([height - margin.bottom, margin.top])
var xAxis = (g, x, height) => g
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x).ticks(width / 80).tickSizeOuter(0))
var yAxis = (g, y, title) => g
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y))
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".title").data([title]).join("text")
.attr("class", "title")
.attr("x", -margin.left)
.attr("y", 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text(title))
focus();
一个选项在下面的步骤中-输出在下面的代码片段中,并且在jsfiddle中同样有效。
data
的小更新-看评论,主要使用value
而不是close
- 首先在
focus
和chart
之外引入可观察单元格值,并包括width
-我在update
块中使用了稍后在函数 中的代码 - 对
chart
的主要更改是删除return
部分,并在chartUpdate
函数中处理chart
渲染,该函数从brush
处理程序调用。 focus
不需要是返回svg.node()
的函数——这是完全有效的,但需要推断它的含义。笔刷的开始和结束日期可以直接传递给chartUpdate
,而不需要在svg.property
中存储值并通过dispatch
等广播它(尽管这是Observable试图演示的一部分)chartUdpate
是chart
中return
语句的update
逻辑的简单提升,它接受brush
中每个日期范围的缩放。
// data
// Changes
// 1. use utcDays because scale is scaleUtc
// 2. rename x and y to xData and yData as x and y are scales
// 3. return 'value' not 'close'
// 4. add the y property for the axis label
const xData = d3.utcDays(new Date(2010, 06, 01), new Date(2020, 10, 30));
var yData = Array.from({length: xData.length}, Math.random).map(n => Math.floor(n * 10) + 5);
var data = xData.map((v, i) => {
return {
"date": v,
"value": yData[i]
}
});
Object.assign(data, {y: "↑ Close $"});
// lift from Observable - neither the focus or chart cells
// Changes
// 1. add width as Observable has it as a built-in
// 2. remove the update block
const height = 440;
const focusHeight = 100;
const margin = {top: 20, right: 20, bottom: 30, left: 40}
// add width
const width = 600;
const xAxis = (g, x, height) => g
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x).ticks(width / 80).tickSizeOuter(0));
const yAxis = (g, y, title) => g
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y))
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".title").data([title]).join("text")
.attr("class", "title")
.attr("x", -margin.left)
.attr("y", 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text(title));
const x = d3.scaleUtc()
.domain(d3.extent(data, d => d.date))
.range([margin.left, width - margin.right]);
const y = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)])
.range([height - margin.bottom, margin.top]);
const area = (x, y) => d3.area()
.defined(d => !isNaN(d.value))
.x(d => x(d.date))
.y0(y(0))
.y1(d => y(d.value));
// Lift from chart block (first)
// Changes
// 1. need select, append not create
// 2. define a clipPath, discard the use of clipId
// 3. reference the clipPath in chartPth
// 4. rename some vars to specify they are for the chart
// 5. the logic in the return statement deferred to later
const svgChart = d3.select("#viz")
.append("svg")
.attr("viewBox", [0, 0, width, height])
.style("display", "block");
// replace clipId and use clippath
svgChart.append("defs")
.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("x", margin.left)
.attr("width", width - margin.left - margin.right)
.attr("height", height);
const chartGx = svgChart.append("g");
const chartGy = svgChart.append("g");
// refer to the clippath
const chartPath = svgChart.append("path")
.datum(data)
.attr("clip-path", "url(#clip)")
.attr("fill", "steelblue");
// lift from focus block (second)
// Changes
// 1. need select, append not create
// 2. change brushed function; don't use dispatch
// 3. svg.node() becomes svg.node().value (as in data 'value')
// 4. chartUpdate is new function with logic from chart cell
const svgFocus = d3.select("#viz")
.append("svg")
.attr("viewBox", [0, 0, width, focusHeight])
.style("display", "block");
const brush = d3.brushX()
.extent([[margin.left, 0.5], [width - margin.right, focusHeight - margin.bottom + 0.5]])
.on("brush", brushed)
.on("end", brushended);
const defaultSelection = [x(d3.utcYear.offset(x.domain()[1], -1)), x.range()[1]];
svgFocus.append("g")
.call(xAxis, x, focusHeight);
svgFocus.append("path")
.datum(data)
.attr("fill", "steelblue")
.attr("d", area(x, y.copy().range([focusHeight - margin.bottom, 4])));
const gb = svgFocus.append("g")
.call(brush)
.call(brush.move, defaultSelection);
// update brushed to sync chart with focus
function brushed({selection}) {
if (selection) {
// update logic goes here - to get args for chartUpdate
const [minX, maxX] = selection.map(x.invert, x).map(d3.utcDay.round);
const maxY = d3.max(data, d => minX <= d.date && d.date <= maxX ? d.value : NaN);
const focusX = x.copy().domain([minX, maxX]);
const focusY = y.copy().domain([0, maxY]);
// call chart update
chartUpdate(focusX, focusY);
}
}
function brushended({selection}) {
if (!selection) {
gb.call(brush.move, defaultSelection);
}
}
// chart update
function chartUpdate(focusX, focusY) {
chartGx.call(xAxis, focusX, height);
chartGy.call(yAxis, focusY, data.y);
chartPath.attr("d", area(focusX, focusY));
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.3.1/d3.min.js"></script>
<div id="viz"></div>