Public
Edited
Sep 13, 2024
Importers
1 star
Insert cell
Insert cell
RadialScatterplot(dataAllWd, {
theta: d => d.wd_006,
thetaDomain: [0, 360],
thetaTicks: [90, 180, 270],
thetaTickFormat: (d) => `${d}°`,
r: d => d.ws_006,
rDomain: [0, d3.max(dataAllWd, d => d.ws_006)],
rLabel: "Wind speed (m/s)",
color: d => d.pow_006,
colorLabel: "Power (kW)",
radius: 1,
radiusTicks: [500, 2000],
radiusTickFormat: (d) => `${d} kW`,
marginTop: 30,
marginRight: 40,
marginBottom: 40,
marginLeft: 40,
strokeOpacity: 1,
fontSize: 14,
})
Insert cell
RadialScatterplot(dataAllWd.filter(d => d.time.getUTCMonth() === 2 && d.time.getUTCDate() === 1), {
marginTop: 20,
marginRight: 40,
marginBottom: 40,
marginLeft: 40,
strokeOpacity: 0.1,
theta: d => d.wd_006,
thetaDomain: [0, 360],
thetaTicks: [90, 180, 270],
thetaTickFormat: (d) => `${d}°`,
r: d => d.ws_006,
rLabel: "Wind speed (m/s)",
radius: d => d.pow_006,
radiusTicks: [500, 2000],
radiusTickFormat: (d) => `${d} kW`,
})
Insert cell
// Modified from https://observablehq.com/@d3/scatterplot
// Copyright 2021 Observable, Inc.
// Released under the ISC license.
function RadialScatterplot(data, {
// dimensions
width = 500, // outer width, in pixels
height = 500, // outer height, in pixels
margin = 0, // margins around the plot
marginLeft = margin,
marginTop = margin,
marginRight = margin,
marginBottom = margin,
strokeWidth = 1.5, // stroke width for dots
strokeOpacity = 1, // stroke opacity for dots,
fontSize = 10,
// theta (angle, like the x-axis)
theta = (d) => d.theta, // given d in data, returns the (quantitative) theta value
thetaDomain, // min and max data values for theta scale
thetaRange = [0, 2 * Math.PI], // min and max degrees
thetaType = d3.scaleLinear, // type of scale
thetaLabel, // a label for the axis
thetaTicks = 5, // number of ticks or array of tick values
thetaTickFormat = "~s", // format string or function
// r (like the y-axis)
r = (d) => d.r, // given d in data, returns the (quantitative) r value
rDomain, // min and max data values for r-scale
rRange = [
0,
Math.min(
width - marginLeft - marginRight,
height - marginTop - marginBottom
) / 2
], // inner and outer radii
rType = d3.scaleLinear, // type of scale
rLabel, // a label for the r-axis
rTicks = 5, // number of ticks or array of tick values
rTickFormat = "~s", // format string or function
// color of the dots
color = "black", // either a constant value or an accessor function like for theta and r
colorDomain, // domain of color scale
colorRange = d3.interpolateBlues, // color scale range or interpolator
colorType = d3.scaleSequential, // type of color scale
colorLabel, // a label for the color legend
colorTicks = 5, // number of ticks or array of tick values
colorTickFormat = "~s", // format string or function
// radius of the dots
radius = 2, // either a constant value or an accessor function like for theta and r
radiusDomain, // min and max data values for radius scale
radiusRange = [1, 10], // min and max radii of dots
radiusType = d3.scaleSqrt, // type of radius scale
radiusLabel, // a label for the radius legend
radiusTicks = 5, // number of ticks or array of tick values for radius legend
radiusTickFormat = "~s", // format string or function
} = {}) {
const isRadiusFunction = typeof radius === "function";
const isColorFunction = typeof color === "function";

// Compute default domains.
if (thetaDomain === undefined) thetaDomain = d3.extent(data, theta);
if (rDomain === undefined) rDomain = d3.extent(data, r);
if (radiusDomain === undefined && isRadiusFunction) radiusDomain = d3.extent(data, radius);
if (colorDomain === undefined && isColorFunction) colorDomain = d3.extent(data, color);

// Construct scales.
const thetaScale = thetaType(thetaDomain, thetaRange);
const rScale = rType(rDomain, rRange).nice();
const radiusScale = isRadiusFunction ? radiusType(radiusDomain, radiusRange) : null;
const colorScale = isColorFunction ? colorType(colorDomain, colorRange) : null;

// Create canvas.
const ctx = DOM.context2d(width, height);
ctx.font = `${fontSize}px sans-serif`;

function drawDots(data) {
ctx.save();

ctx.translate(marginLeft + rRange[1], marginTop + rRange[1]);
ctx.globalAlpha = strokeOpacity;
ctx.lineWidth = strokeWidth;
data.forEach(d => {
ctx.beginPath();
const hyp = rScale(r(d))
const angle = thetaScale(theta(d)) - Math.PI / 2;
const cx = hyp * Math.cos(angle);
const cy = hyp * Math.sin(angle);
const dotRadius = isRadiusFunction ? radiusScale(radius(d)) : radius;
const dotColor = isColorFunction ? colorScale(color(d)) : color;
ctx.arc(cx, cy, dotRadius, 0, 2 * Math.PI);
ctx.strokeStyle = dotColor;
ctx.stroke();
});
ctx.restore();
}

function drawRAxis() {
ctx.save();

ctx.translate(marginLeft + rRange[1], marginTop + rRange[1]);
const rFormat = getFormat(rTickFormat);
const rTickValues = Array.isArray(rTicks) ? rTicks : rScale.ticks(rTicks);
ctx.strokeStyle = "black";
ctx.globalAlpha = 0.5;
ctx.lineWidth = 1;
ctx.beginPath();
rTickValues.slice(1).forEach((v) => {
ctx.moveTo(rScale(v), 0);
ctx.arc(0, 0, rScale(v), 0, 2 * Math.PI);
});
ctx.stroke();
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.strokeStyle = 'white';
ctx.fillStyle = 'black';
ctx.globalAlpha = 1;
ctx.lineWidth = 5;
rTickValues.slice(1).forEach((v) => {
const cx = 0;
const cy = -rScale(v);
ctx.strokeText(rFormat(v), cx, cy);
ctx.fillText(rFormat(v), cx, cy);
});
ctx.strokeText(rLabel, 0, -rScale(rTickValues[rTickValues.length - 1]) - fontSize);
ctx.fillText(rLabel, 0, -rScale(rTickValues[rTickValues.length - 1]) - fontSize);

ctx.restore();
}

function drawThetaAxis() {
ctx.save();

ctx.translate(marginLeft + rRange[1], marginTop + rRange[1]);
const thetaFormat = getFormat(thetaTickFormat);
const thetaTickValues = Array.isArray(thetaTicks) ? thetaTicks : thetaScale.ticks(thetaTicks);
ctx.strokeStyle = "black";
ctx.globalAlpha = 1;
ctx.lineWidth = 1;
ctx.beginPath();
thetaTickValues.forEach((v) => {
ctx.save();
const radians = thetaScale(v) - Math.PI / 2;;
ctx.rotate(radians);
ctx.moveTo(rRange[1], 0);
ctx.lineTo(rRange[1] + 6, 0);
ctx.restore();
});
ctx.stroke();
ctx.textAlign = 'start';
ctx.textBaseline = 'middle';
ctx.fillStyle = 'black';
ctx.globalAlpha = 1;
thetaTickValues.forEach((v) => {
ctx.save();
ctx.rotate(thetaScale(v) - Math.PI / 2);
ctx.fillText(thetaFormat(v), rRange[1] + 9, 0);
ctx.restore();
});

ctx.restore();
}

function update(data) {
ctx.clearRect(0, 0, width, height);
drawDots(data);
drawRAxis();
drawThetaAxis();
}

update(data);

const div = d3.create("div");

if (isColorFunction) {
const colorFormat = getFormat(colorTickFormat);
const colorTickValues = Array.isArray(colorTicks) ? colorTicks : colorScale.ticks(colorTicks);
const legend = Legend(colorScale, {
tickFormat: colorFormat,
tickValues: colorTickValues,
title: colorLabel
});

d3.select(legend)
.selectAll("text")
.attr("font-size", fontSize);

div.append(() => legend);
}

if (isRadiusFunction) {
const radiusFormat = getFormat(radiusTickFormat);
const radiusTickValues = Array.isArray(radiusTicks) ? radiusTicks : radiusScale.ticks(radiusTicks);
const circleLegend = legendCircle()
.scale(radiusScale)
.tickValues(radiusTickValues)
.tickFormat(radiusFormat)
.tickSize(5);

const svg = d3.create("svg")
.attr("style", "display: block;")
.attr("height", radiusScale.range()[1] * 2 + 40)
.attr("width", radiusScale.range()[1] * 2 + 100);
// Call it on a g element
svg.append("g")
.attr("transform", "translate(0, 20)")
.call(circleLegend);

div.append(() => svg.node());
};

div.append(() => ctx.canvas);
return Object.assign(div.node(), {
update,
scales: {
theta: thetaScale,
r: rScale,
radius: radiusScale,
color: colorScale
}
});
}
Insert cell
import {dataAllWd} from "19c78c642404d0ce"
Insert cell
dataAllWd
Insert cell
function getFormat(format) {
// From https://observablehq.com/@d3/color-legend
return format === undefined ? d => d
: typeof format === "string" ? d3.format(format)
: format;
}
Insert cell
import {Legend} from "@d3/color-legend"
Insert cell
import {legendCircle} from "@harrystevens/circle-legend";
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more