Public
Edited
Jul 12, 2023
Insert cell
Insert cell
{
const ard = d3.arc()
.innerRadius(80)
.outerRadius(100)
.startAngle(-Math.PI/2)
.endAngle(Math.PI / 2);
const svg = d3.create('svg').attr('width', 220).attr('height', 220);
svg.append('path').attr('d', ard).style('fill', 'green').attr("transform", `translate(100, 100)`);
return svg.node()
}
Insert cell
Insert cell
config = {
return {
startAngle: -135,
endAngle: 135,
radius: 88,
bgCircleStroke: 8,
valueCircleStroke: 16,
width: 200,
height: 200,
margin: 12,
min: 0,
max: 200,
centerRadius: 8
}
}
Insert cell
function degreeToRadian(angle) {
return angle * Math.PI / 180;
}
Insert cell
valueScale = d3.scaleLinear([config.min, config.max], [degreeToRadian(config.startAngle), degreeToRadian(config.endAngle)])
Insert cell
valueScale(0) * 180 / Math.PI
Insert cell
viewof realValue = Inputs.range([0, 200], {label: "实时值", step: 5, value: 80})
Insert cell
// {
// // 可以注释这个 cell 运行动画
// pointer.data([40])
// triggerPointerTransition(190, 300)
// }
Insert cell
{
const svg = html`<svg></svg>`
const svgSelection = d3.select(svg).attr('width', config.width).attr('height', config.height);
const bgCircle = d3.arc()
.startAngle(config.startAngle * Math.PI / 180)
.endAngle(config.endAngle * Math.PI/180)
.innerRadius(config.radius - config.bgCircleStroke / 2)
.outerRadius(config.radius + config.bgCircleStroke / 2)
.cornerRadius(config.bgCircleStroke);

svgSelection.append('path').attr('d', bgCircle).style("fill", "#DBE8E4").attr('transform', `translate(100, 100)`);

const valueCirleGroup = svgSelection.append('g');
const valueCircle = d3.arc()
.startAngle(valueScale(0))
.endAngle(valueScale(realValue))
.innerRadius(config.radius - config.valueCircleStroke / 2)
.outerRadius(config.radius + config.valueCircleStroke / 2)
.cornerRadius(config.valueCircleStroke);

const circleGradient = appendGradient(svg);
valueCirleGroup
.append('path')
.attr('d', valueCircle)
.attr('filter', "url(#filterRing)") // 注意这里使用的是后文中的 filter
// .style('filter', 'drop-shadow( 3px 3px 2px rgba(0, 0, 0, .2))')
.attr('fill', `url(#${circleGradient.attr('id')})`)
.attr('stroke', "none")
.attr('transform', `translate(100, 100)`)

// const centerPoint = d3.arc()
// .startAngle(0)
// .endAngle(2*Math.PI)
// .innerRadius(0)
// .outerRadius(config.centerRadius)

const centerPoint = svgSelection.append('circle');
centerPoint
.attr('id', 'center-point')
.attr('cx', config.radius + config.margin)
.attr('cy', config.radius + config.margin)
.attr('r', config.centerRadius)
.attr('filter', "url(#filterCenter)") // 注意这里使用的是后文中的 filter
// .style('filter', 'drop-shadow( 3px 3px 2px rgba(0, 0, 0, 0.2))')
.attr('fill', '#333');

// const degree = valueScale(realValue) * 180/ Math.PI;
// const degree = - 135
mutable pointer = svgSelection
.insert('g', "#center-point")
.selectAll('path')
.data([realValue])
.join("path")
.attr('d', pointerString)
.attr('fill', '#ff4242')
.attr('transform', d => `rotate(${valueScale(d) * 180/ Math.PI}, 100, 100) translate(61, 40)`)



const innerRadius = config.radius - config.valueCircleStroke / 2 - 2
const TICK_LEN =4 ;
const BIG_TICK_LEN = 10;
svgSelection.append('g')
.attr('id', 'tick-group')
.attr('transform', `translate(100, 100)`)
.selectAll()
.data(ticks)
.join('line')
.attr("x1", angle => innerRadius * Math.cos(angle))
.attr("y1", angle => innerRadius * Math.sin(angle))
.attr("x2", angle => (innerRadius - TICK_LEN) * Math.cos(angle))
.attr("y2", angle => (innerRadius - TICK_LEN) * Math.sin(angle))
.attr('stroke', '#00C1B2')
.attr('stroke-width', 1)

svgSelection.append('g')
.attr('id', 'big-tick-group')
.attr('transform', `translate(100, 100)`)
.selectAll()
.data(bigTicks)
.join('line')
.attr("x1", angle => (innerRadius) * Math.cos(angle))
.attr("y1", angle => (innerRadius) * Math.sin(angle))
.attr("x2", angle => (innerRadius - 10) * Math.cos(angle))
.attr("y2", angle => (innerRadius - 10) * Math.sin(angle))
.attr('stroke-width', 2)
.attr('stroke', '#00C1B2')

const filterDefsGroup = svgSelection.append('defs')
filterDefsGroup.node().appendChild(filterRing)
filterDefsGroup.node().appendChild(filterCenter)


return svg
}

Insert cell
function appendGradient(svg) {


const id = 'gradient-test'
const gradient = d3.select(svg).append('linearGradient'); // this is a selection
gradient.attr('id', id)
.attr("gradientUnits", "userSpaceOnUse")
.attr('x1', config.margin)
.attr('y1', config.height - config.margin)
// .attr('x2', config.margin + 2 * config.radius)
.attr('x2', config.width * 1.5)
.attr('y2', config.margin)
.selectAll('stop')
.data([0,1])
.join('stop')
.attr('offset', d => d)
.attr('stop-color', d => d === 0? '#00C1B2' : '#6DFAB7')

return gradient
}
Insert cell
pointerString = "M39.0003 77.1836L33.3791 61.7256L39.0004 0.816238L44.6217 61.7256Z"
Insert cell
d3.ticks(0,1,10)
Insert cell
Insert cell
Insert cell
// ticks = d3.ticks(config.min, config.max, 60).map(valueScale).map(d => d -Math.PI/2
ticks= getTicks(config.min, config.max, 60).map(valueScale).map(d => d -Math.PI/2)
Insert cell
// bigTicks = d3.ticks(config.min, config.max, 6).map(valueScale).map(d => d -Math.PI/2)
bigTicks = getTicks(config.min, config.max, 6).map(valueScale).map(d => d -Math.PI/2)
Insert cell
function getTicks(start, end, count) {
const step = (end- start) / count;
let arr = []
for ( let i=0; i<=count; i++) {
arr.push(start + i * step)
}
return arr;
}
Insert cell
function createCreateRotateTween(target) {
const nextAngle = valueScale(target) * 180/ Math.PI
console.log(target)
return function createRotateTween(d) {
console.log('initial data: ', d);
const currentAngle = valueScale(d) * 180/ Math.PI
const interpolate = d3.interpolate(currentAngle, nextAngle);
return function(t) {
const val = `rotate(${interpolate(t)}, 100, 100) translate(61, 40)`
console.log(val, "currenAngle", currentAngle, "nextAngle", nextAngle)
return val
}
}
}
Insert cell
mutable pointer = null;
Insert cell
function triggerPointerTransition(target, duration = 700) {
pointer.transition().duration(duration).ease(d3.easeCubicIn)
.attrTween('transform', createCreateRotateTween(target))
}
Insert cell
md`## 阴影 Filter 代码`
Insert cell
Insert cell
filterCenter = svg`<filter id="filterCenter" width="50" height="50" x="75" y="75" color-interpolation-filters="sRGB"
filterUnits="userSpaceOnUse">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" />
<feOffset />
<feGaussianBlur stdDeviation="4" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.4 0" />
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_2003_43232" />
<feBlend in="SourceGraphic" in2="effect1_dropShadow_2003_43232" result="shape" />
</filter>`

Insert cell
Insert cell
filterRing = svg`<filter id="filterRing" width="200" height="200" x="-100" y="-100" color-interpolation-filters="sRGB"
filterUnits="userSpaceOnUse">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" />
<feMorphology in="SourceAlpha" radius="1" result="effect1_dropShadow_2003_43232" />
<feOffset dy="2" />
<feGaussianBlur stdDeviation="2" />
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0" />
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_2003_43232" />
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" />
<feOffset dy="4" />
<feGaussianBlur stdDeviation="2.5" />
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.08 0" />
<feBlend in2="effect1_dropShadow_2003_43232" result="effect2_dropShadow_2003_43232" />
<feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" />
<feOffset dy="1" />
<feGaussianBlur stdDeviation="5" />
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" />
<feBlend in2="effect2_dropShadow_2003_43232" result="effect3_dropShadow_2003_43232" />
<feBlend in="SourceGraphic" in2="effect3_dropShadow_2003_43232" result="shape" />
</filter>`
Insert cell
filterShalow = svg`<filter id="shadow" x="0" y="0" width="200%" height="200%">
<feDropShadow dx="3" dy="3" stdDeviation="1" flood-color="#ff0000" flood-opacity="1" />
</filter>`
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more