diff --git a/src/components/CountryTimeline.ts b/src/components/CountryTimeline.ts index e4d6a3ca7..27feb7ebf 100644 --- a/src/components/CountryTimeline.ts +++ b/src/components/CountryTimeline.ts @@ -1,4 +1,14 @@ -import * as d3 from 'd3'; +import { + select, + scaleTime, + scaleBand, + axisBottom, + timeFormat, + type Selection, + type ScaleTime, + type ScaleBand, + type NumberValue, +} from 'd3'; import { escapeHtml } from '@/utils/sanitize'; import { getCSSColor } from '@/utils'; import { t } from '@/services/i18n'; @@ -32,7 +42,7 @@ const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000; export class CountryTimeline { private container: HTMLElement; - private svg: d3.Selection | null = null; + private svg: Selection | null = null; private tooltip: HTMLDivElement | null = null; private resizeObserver: ResizeObserver | null = null; private currentEvents: TimelineEvent[] = []; @@ -87,8 +97,7 @@ export class CountryTimeline { const innerW = width - MARGIN.left - MARGIN.right; const innerH = HEIGHT - MARGIN.top - MARGIN.bottom; - this.svg = d3 - .select(this.container) + this.svg = select(this.container) .append('svg') .attr('width', width) .attr('height', HEIGHT) @@ -99,13 +108,11 @@ export class CountryTimeline { .attr('transform', `translate(${MARGIN.left},${MARGIN.top})`); const now = Date.now(); - const xScale = d3 - .scaleTime() + const xScale = scaleTime() .domain([new Date(now - SEVEN_DAYS_MS), new Date(now)]) .range([0, innerW]); - const yScale = d3 - .scaleBand() + const yScale = scaleBand() .domain(LANES) .range([0, innerH]) .padding(0.2); @@ -118,8 +125,8 @@ export class CountryTimeline { } private drawGrid( - g: d3.Selection, - xScale: d3.ScaleTime, + g: Selection, + xScale: ScaleTime, innerH: number, ): void { const ticks = xScale.ticks(6); @@ -135,15 +142,14 @@ export class CountryTimeline { } private drawAxes( - g: d3.Selection, - xScale: d3.ScaleTime, - yScale: d3.ScaleBand, + g: Selection, + xScale: ScaleTime, + yScale: ScaleBand, innerH: number, ): void { - const xAxis = d3 - .axisBottom(xScale) + const xAxis = axisBottom(xScale) .ticks(6) - .tickFormat(d3.timeFormat('%b %d') as (d: Date | d3.NumberValue, i: number) => string); + .tickFormat(timeFormat('%b %d') as (d: Date | NumberValue, i: number) => string); const xAxisG = g .append('g') @@ -175,8 +181,8 @@ export class CountryTimeline { } private drawNowMarker( - g: d3.Selection, - xScale: d3.ScaleTime, + g: Selection, + xScale: ScaleTime, now: Date, innerH: number, ): void { @@ -201,9 +207,9 @@ export class CountryTimeline { } private drawEmptyLaneLabels( - g: d3.Selection, + g: Selection, events: TimelineEvent[], - yScale: d3.ScaleBand, + yScale: ScaleBand, innerW: number, ): void { const populatedLanes = new Set(events.map((e) => e.lane)); @@ -223,14 +229,14 @@ export class CountryTimeline { } private drawEvents( - g: d3.Selection, + g: Selection, events: TimelineEvent[], - xScale: d3.ScaleTime, - yScale: d3.ScaleBand, + xScale: ScaleTime, + yScale: ScaleBand, ): void { const tooltip = this.tooltip!; const container = this.container; - const fmt = d3.timeFormat('%b %d, %H:%M'); + const fmt = timeFormat('%b %d, %H:%M'); g.selectAll('.event-circle') .data(events) @@ -244,7 +250,7 @@ export class CountryTimeline { .attr('stroke', getCSSColor('--shadow-color')) .attr('stroke-width', 0.5) .on('mouseenter', function (event: MouseEvent, d: TimelineEvent) { - d3.select(this).attr('opacity', 1).attr('stroke', getCSSColor('--text')).attr('stroke-width', 1.5); + select(this).attr('opacity', 1).attr('stroke', getCSSColor('--text')).attr('stroke-width', 1.5); const dateStr = fmt(new Date(d.timestamp)); tooltip.innerHTML = `${escapeHtml(d.label)}
${escapeHtml(dateStr)}`; tooltip.style.display = 'block'; @@ -262,7 +268,7 @@ export class CountryTimeline { tooltip.style.top = `${y}px`; }) .on('mouseleave', function () { - d3.select(this).attr('opacity', 0.85).attr('stroke', getCSSColor('--shadow-color')).attr('stroke-width', 0.5); + select(this).attr('opacity', 0.85).attr('stroke', getCSSColor('--shadow-color')).attr('stroke-width', 0.5); tooltip.style.display = 'none'; }); } diff --git a/src/components/Map.ts b/src/components/Map.ts index a61c2c37a..59bc024be 100644 --- a/src/components/Map.ts +++ b/src/components/Map.ts @@ -1,4 +1,14 @@ -import * as d3 from 'd3'; +import { + select, + geoPath, + geoEquirectangular, + geoGraticule, + line as d3Line, + curveCardinal, + type Selection, + type GeoProjection, + type GeoPath, +} from 'd3'; import * as topojson from 'topojson-client'; import { escapeHtml } from '@/utils/sanitize'; import { getCSSColor } from '@/utils'; @@ -104,7 +114,7 @@ export class MapComponent { }; private container: HTMLElement; - private svg: d3.Selection; + private svg: Selection; private wrapper: HTMLElement; private overlays: HTMLElement; private clusterCanvas: HTMLCanvasElement; @@ -113,8 +123,8 @@ export class MapComponent { private worldData: WorldTopology | null = null; private countryFeatures: Feature[] | null = null; private isResizing = false; - private baseLayerGroup: d3.Selection | null = null; - private dynamicLayerGroup: d3.Selection | null = null; + private baseLayerGroup: Selection | null = null; + private dynamicLayerGroup: Selection | null = null; private baseRendered = false; private baseWidth = 0; private baseHeight = 0; @@ -193,7 +203,7 @@ export class MapComponent { container.appendChild(this.createLegend()); this.healthCheckIntervalId = setInterval(() => this.runHealthCheck(), 30000); - this.svg = d3.select(svgElement); + this.svg = select(svgElement); this.baseLayerGroup = this.svg.append('g').attr('class', 'map-base'); this.dynamicLayerGroup = this.svg.append('g').attr('class', 'map-dynamic'); this.popup = new MapPopup(container); @@ -894,7 +904,7 @@ export class MapComponent { this.clusterGl.clear(this.clusterGl.COLOR_BUFFER_BIT); } - private renderClusterLayer(_projection: d3.GeoProjection): void { + private renderClusterLayer(_projection: GeoProjection): void { // WebGL clustering disabled - all layers use HTML markers for visual fidelity // (severity colors, emoji icons, magnitude sizing, animations) this.wrapper.classList.toggle('cluster-active', false); @@ -989,7 +999,7 @@ export class MapComponent { // Setup projection for base elements const baseProjection = this.getProjection(width, height); - const basePath = d3.geoPath().projection(baseProjection); + const basePath = geoPath().projection(baseProjection); // Graticule this.renderGraticule(this.baseLayerGroup, basePath); @@ -1051,7 +1061,7 @@ export class MapComponent { } private renderGrid( - group: d3.Selection, + group: Selection, width: number, height: number, yStart = 0 @@ -1081,7 +1091,7 @@ export class MapComponent { } } - private getProjection(width: number, height: number): d3.GeoProjection { + private getProjection(width: number, height: number): GeoProjection { // Equirectangular with cropped latitude range (72°N to 56°S = 128°) // Shows Greenland/Iceland while trimming extreme polar regions const LAT_NORTH = 72; // Includes Greenland (extends to ~83°N but 72 shows most) @@ -1094,18 +1104,17 @@ export class MapComponent { const scaleForHeight = height / (LAT_RANGE * Math.PI / 180); const scale = Math.min(scaleForWidth, scaleForHeight); - return d3 - .geoEquirectangular() + return geoEquirectangular() .scale(scale) .center([0, LAT_CENTER]) .translate([width / 2, height / 2]); } private renderGraticule( - group: d3.Selection, - path: d3.GeoPath + group: Selection, + path: GeoPath ): void { - const graticule = d3.geoGraticule(); + const graticule = geoGraticule(); group .append('path') .datum(graticule()) @@ -1117,8 +1126,8 @@ export class MapComponent { } private renderCountries( - group: d3.Selection, - path: d3.GeoPath + group: Selection, + path: GeoPath ): void { if (!this.countryFeatures) return; @@ -1134,16 +1143,15 @@ export class MapComponent { .attr('stroke-width', 0.7); } - private renderCables(projection: d3.GeoProjection): void { + private renderCables(projection: GeoProjection): void { if (!this.dynamicLayerGroup) return; const cableGroup = this.dynamicLayerGroup.append('g').attr('class', 'cables'); UNDERSEA_CABLES.forEach((cable) => { - const lineGenerator = d3 - .line<[number, number]>() + const lineGenerator = d3Line<[number, number]>() .x((d) => projection(d)?.[0] ?? 0) .y((d) => projection(d)?.[1] ?? 0) - .curve(d3.curveCardinal); + .curve(curveCardinal); const isHighlighted = this.highlightedAssets.cable.has(cable.id); const cableAdvisory = this.getCableAdvisory(cable.id); @@ -1172,16 +1180,15 @@ export class MapComponent { }); } - private renderPipelines(projection: d3.GeoProjection): void { + private renderPipelines(projection: GeoProjection): void { if (!this.dynamicLayerGroup) return; const pipelineGroup = this.dynamicLayerGroup.append('g').attr('class', 'pipelines'); PIPELINES.forEach((pipeline) => { - const lineGenerator = d3 - .line<[number, number]>() + const lineGenerator = d3Line<[number, number]>() .x((d) => projection(d)?.[0] ?? 0) .y((d) => projection(d)?.[1] ?? 0) - .curve(d3.curveCardinal.tension(0.5)); + .curve(curveCardinal.tension(0.5)); const color = PIPELINE_COLORS[pipeline.type] || getCSSColor('--text-dim'); const opacity = 0.85; @@ -1218,7 +1225,7 @@ export class MapComponent { }); } - private renderConflicts(projection: d3.GeoProjection): void { + private renderConflicts(projection: GeoProjection): void { if (!this.dynamicLayerGroup) return; const conflictGroup = this.dynamicLayerGroup.append('g').attr('class', 'conflicts'); @@ -1250,7 +1257,7 @@ export class MapComponent { const useSanctions = this.state.layers.sanctions; this.baseLayerGroup.selectAll('.country').each(function (datum) { - const el = d3.select(this); + const el = select(this); const id = datum as { id?: number }; if (!useSanctions) { el.attr('fill', defaultFill); @@ -1271,7 +1278,7 @@ export class MapComponent { // groupKey function ensures only items with same key can cluster (e.g., same city) private clusterMarkers( items: T[], - projection: d3.GeoProjection, + projection: GeoProjection, pixelRadius: number, getGroupKey?: (item: T) => string ): Array<{ items: T[]; center: [number, number]; pos: [number, number] }> { @@ -1334,7 +1341,7 @@ export class MapComponent { return clusters; } - private renderOverlays(projection: d3.GeoProjection): void { + private renderOverlays(projection: GeoProjection): void { this.overlays.innerHTML = ''; // Strategic waterways @@ -2708,7 +2715,7 @@ export class MapComponent { } } - private renderWaterways(projection: d3.GeoProjection): void { + private renderWaterways(projection: GeoProjection): void { STRATEGIC_WATERWAYS.forEach((waterway) => { const pos = projection([waterway.lon, waterway.lat]); if (!pos) return; @@ -2738,7 +2745,7 @@ export class MapComponent { }); } - private renderAisDisruptions(projection: d3.GeoProjection): void { + private renderAisDisruptions(projection: GeoProjection): void { this.aisDisruptions.forEach((event) => { const pos = projection([event.lon, event.lat]); if (!pos) return; @@ -2773,7 +2780,7 @@ export class MapComponent { }); } - private renderAisDensity(projection: d3.GeoProjection): void { + private renderAisDensity(projection: GeoProjection): void { if (!this.dynamicLayerGroup) return; const densityGroup = this.dynamicLayerGroup.append('g').attr('class', 'ais-density'); @@ -2799,7 +2806,7 @@ export class MapComponent { }); } - private renderPorts(projection: d3.GeoProjection): void { + private renderPorts(projection: GeoProjection): void { PORTS.forEach((port) => { const pos = projection([port.lon, port.lat]); if (!pos) return; @@ -2834,7 +2841,7 @@ export class MapComponent { }); } - private renderAPTMarkers(projection: d3.GeoProjection): void { + private renderAPTMarkers(projection: GeoProjection): void { APT_GROUPS.forEach((apt) => { const pos = projection([apt.lon, apt.lat]); if (!pos) return; diff --git a/src/components/ProgressChartsPanel.ts b/src/components/ProgressChartsPanel.ts index 4a2c4c2d1..ba4c896b9 100644 --- a/src/components/ProgressChartsPanel.ts +++ b/src/components/ProgressChartsPanel.ts @@ -8,7 +8,20 @@ */ import { Panel } from './Panel'; -import * as d3 from 'd3'; +import { + select, + extent, + scaleLinear, + area, + curveMonotoneX, + line, + axisBottom, + axisLeft, + bisector, + pointer, + type Selection, + type ScaleLinear, +} from 'd3'; import { type ProgressDataSet, type ProgressDataPoint } from '@/services/progress-data'; import { getCSSColor } from '@/utils'; import { replaceChildren } from '@/utils/dom-utils'; @@ -158,7 +171,7 @@ export class ProgressChartsPanel extends Panel { const width = containerWidth - CHART_MARGIN.left - CHART_MARGIN.right; const height = CHART_HEIGHT; - const svg = d3.select(container) + const svg = select(container) .append('svg') .attr('width', containerWidth) .attr('height', height + CHART_MARGIN.top + CHART_MARGIN.bottom) @@ -168,48 +181,48 @@ export class ProgressChartsPanel extends Panel { .attr('transform', `translate(${CHART_MARGIN.left},${CHART_MARGIN.top})`); // Scales - const xExtent = d3.extent(data, d => d.year) as [number, number]; - const yExtent = d3.extent(data, d => d.value) as [number, number]; + const xExtent = extent(data, d => d.year) as [number, number]; + const yExtent = extent(data, d => d.value) as [number, number]; const yPadding = (yExtent[1] - yExtent[0]) * 0.1; - const x = d3.scaleLinear() + const x = scaleLinear() .domain(xExtent) .range([0, width]); - const y = d3.scaleLinear() + const y = scaleLinear() .domain([yExtent[0] - yPadding, yExtent[1] + yPadding]) .range([height, 0]); // Area generator with smooth curve - const area = d3.area() + const areaGen = area() .x(d => x(d.year)) .y0(height) .y1(d => y(d.value)) - .curve(d3.curveMonotoneX); + .curve(curveMonotoneX); // Line generator for top edge - const line = d3.line() + const lineGen = line() .x(d => x(d.year)) .y(d => y(d.value)) - .curve(d3.curveMonotoneX); + .curve(curveMonotoneX); // Filled area g.append('path') .datum(data) - .attr('d', area) + .attr('d', areaGen) .attr('fill', color) .attr('opacity', 0.2); // Stroke line g.append('path') .datum(data) - .attr('d', line) + .attr('d', lineGen) .attr('fill', 'none') .attr('stroke', color) .attr('stroke-width', 2); // X axis - const xAxis = d3.axisBottom(x) + const xAxis = axisBottom(x) .ticks(Math.min(5, data.length)) .tickFormat(d => String(d)); @@ -224,7 +237,7 @@ export class ProgressChartsPanel extends Panel { xAxisG.select('.domain').attr('stroke', 'var(--border-subtle)'); // Y axis - const yAxis = d3.axisLeft(y) + const yAxis = axisLeft(y) .ticks(3) .tickFormat(d => formatAxisValue(d as number)); @@ -245,10 +258,10 @@ export class ProgressChartsPanel extends Panel { * Add mouse hover tooltip interaction to a chart. */ private addHoverInteraction( - g: d3.Selection, + g: Selection, data: ProgressDataPoint[], - x: d3.ScaleLinear, - y: d3.ScaleLinear, + x: ScaleLinear, + y: ScaleLinear, width: number, height: number, color: string, @@ -257,7 +270,7 @@ export class ProgressChartsPanel extends Panel { const tooltip = this.tooltip; if (!tooltip) return; - const bisector = d3.bisector(d => d.year).left; + const bisect = bisector(d => d.year).left; // Invisible overlay rect for mouse events const overlay = g.append('rect') @@ -283,9 +296,9 @@ export class ProgressChartsPanel extends Panel { overlay .on('mousemove', (event: MouseEvent) => { - const [mx] = d3.pointer(event, overlay.node()!); + const [mx] = pointer(event, overlay.node()!); const yearVal = x.invert(mx); - const idx = bisector(data, yearVal, 1); + const idx = bisect(data, yearVal, 1); const d0 = data[idx - 1]; const d1 = data[idx]; if (!d0) return; diff --git a/src/components/RenewableEnergyPanel.ts b/src/components/RenewableEnergyPanel.ts index 09b398c18..05b16886b 100644 --- a/src/components/RenewableEnergyPanel.ts +++ b/src/components/RenewableEnergyPanel.ts @@ -7,7 +7,22 @@ */ import { Panel } from './Panel'; -import * as d3 from 'd3'; +import { + select, + arc, + interpolate, + easeCubicOut, + extent, + scaleLinear, + area, + curveMonotoneX, + line, + stack, + stackOrderNone, + stackOffsetNone, + max, + type SeriesPoint, +} from 'd3'; import type { RenewableEnergyData, RegionRenewableData, CapacitySeries } from '@/services/renewable-energy-data'; import { getCSSColor } from '@/utils'; import { replaceChildren } from '@/utils/dom-utils'; @@ -91,7 +106,7 @@ export class RenewableEnergyPanel extends Panel { const innerRadius = radius * 0.7; const outerRadius = radius; - const svg = d3.select(container) + const svg = select(container) .append('svg') .attr('viewBox', `0 0 ${size} ${size}`) .attr('width', size) @@ -102,7 +117,7 @@ export class RenewableEnergyPanel extends Panel { .attr('transform', `translate(${radius},${radius})`); // Arc generator - const arc = d3.arc() + const arcGen = arc() .innerRadius(innerRadius) .outerRadius(outerRadius) .cornerRadius(4) @@ -111,23 +126,23 @@ export class RenewableEnergyPanel extends Panel { // Background arc (full circle) -- theme-aware track color g.append('path') .datum({ endAngle: Math.PI * 2 }) - .attr('d', arc as any) + .attr('d', arcGen as any) .attr('fill', getCSSColor('--border')); // Foreground arc (renewable %) -- animated from 0 to target const targetAngle = (percentage / 100) * Math.PI * 2; const foreground = g.append('path') .datum({ endAngle: 0 }) - .attr('d', arc as any) + .attr('d', arcGen as any) .attr('fill', getCSSColor('--green')); // Animate the arc from 0 to target percentage - const interpolate = d3.interpolate(0, targetAngle); + const angleInterpolator = interpolate(0, targetAngle); foreground.transition() .duration(1500) - .ease(d3.easeCubicOut) + .ease(easeCubicOut) .attrTween('d', () => (t: number) => { - return (arc as any)({ endAngle: interpolate(t) }); + return (arcGen as any)({ endAngle: angleInterpolator(t) }); }); // Center text: percentage value @@ -178,7 +193,7 @@ export class RenewableEnergyPanel extends Panel { if (width <= 0) return; - const svg = d3.select(container) + const svg = select(container) .append('svg') .attr('width', containerWidth) .attr('height', height + margin.top + margin.bottom) @@ -187,39 +202,39 @@ export class RenewableEnergyPanel extends Panel { const g = svg.append('g') .attr('transform', `translate(${margin.left},${margin.top})`); - const xExtent = d3.extent(historicalData, d => d.year) as [number, number]; - const yExtent = d3.extent(historicalData, d => d.value) as [number, number]; + const xExtent = extent(historicalData, d => d.year) as [number, number]; + const yExtent = extent(historicalData, d => d.value) as [number, number]; const yPadding = (yExtent[1] - yExtent[0]) * 0.1; - const x = d3.scaleLinear().domain(xExtent).range([0, width]); - const y = d3.scaleLinear() + const x = scaleLinear().domain(xExtent).range([0, width]); + const y = scaleLinear() .domain([yExtent[0] - yPadding, yExtent[1] + yPadding]) .range([height, 0]); const greenColor = getCSSColor('--green'); // Area fill - const area = d3.area<{ year: number; value: number }>() + const sparkArea = area<{ year: number; value: number }>() .x(d => x(d.year)) .y0(height) .y1(d => y(d.value)) - .curve(d3.curveMonotoneX); + .curve(curveMonotoneX); g.append('path') .datum(historicalData) - .attr('d', area) + .attr('d', sparkArea) .attr('fill', greenColor) .attr('opacity', 0.15); // Line stroke - const line = d3.line<{ year: number; value: number }>() + const sparkLine = line<{ year: number; value: number }>() .x(d => x(d.year)) .y(d => y(d.value)) - .curve(d3.curveMonotoneX); + .curve(curveMonotoneX); g.append('path') .datum(historicalData) - .attr('d', line) + .attr('d', sparkLine) .attr('fill', 'none') .attr('stroke', greenColor) .attr('stroke-width', 1.5); @@ -379,28 +394,28 @@ export class RenewableEnergyPanel extends Panel { if (innerWidth <= 0) return; // D3 stack for solar + wind - const stack = d3.stack<{ year: number; solar: number; wind: number }>() + const stackGen = stack<{ year: number; solar: number; wind: number }>() .keys(['solar', 'wind']) - .order(d3.stackOrderNone) - .offset(d3.stackOffsetNone); + .order(stackOrderNone) + .offset(stackOffsetNone); - const stacked = stack(combinedData); + const stacked = stackGen(combinedData); // Scales - const xScale = d3.scaleLinear() + const xScale = scaleLinear() .domain([sortedYears[0]!, sortedYears[sortedYears.length - 1]!]) .range([0, innerWidth]); - const stackedMax = d3.max(stacked, layer => d3.max(layer, d => d[1])) ?? 0; - const coalMax = d3.max(combinedData, d => d.coal) ?? 0; + const stackedMax = max(stacked, layer => max(layer, d => d[1])) ?? 0; + const coalMax = max(combinedData, d => d.coal) ?? 0; const yMax = Math.max(stackedMax, coalMax) * 1.1; // 10% padding - const yScale = d3.scaleLinear() + const yScale = scaleLinear() .domain([0, yMax]) .range([innerHeight, 0]); // Create SVG - const svg = d3.select(container) + const svg = select(container) .append('svg') .attr('width', containerWidth) .attr('height', height) @@ -416,11 +431,11 @@ export class RenewableEnergyPanel extends Panel { const coalColor = getCSSColor('--red'); // Area generator for stacked layers - const areaGen = d3.area>() + const stackArea = area>() .x(d => xScale(d.data.year)) .y0(d => yScale(d[0])) .y1(d => yScale(d[1])) - .curve(d3.curveMonotoneX); + .curve(curveMonotoneX); const fillColors = [solarColor, windColor]; @@ -428,17 +443,17 @@ export class RenewableEnergyPanel extends Panel { stacked.forEach((layer, i) => { g.append('path') .datum(layer) - .attr('d', areaGen) + .attr('d', stackArea) .attr('fill', fillColors[i]!) .attr('opacity', 0.6); }); // Render coal as declining area + line - const coalArea = d3.area<{ year: number; coal: number }>() + const coalArea = area<{ year: number; coal: number }>() .x(d => xScale(d.year)) .y0(innerHeight) .y1(d => yScale(d.coal)) - .curve(d3.curveMonotoneX); + .curve(curveMonotoneX); g.append('path') .datum(combinedData) @@ -446,10 +461,10 @@ export class RenewableEnergyPanel extends Panel { .attr('fill', coalColor) .attr('opacity', 0.2); - const coalLine = d3.line<{ year: number; coal: number }>() + const coalLine = line<{ year: number; coal: number }>() .x(d => xScale(d.year)) .y(d => yScale(d.coal)) - .curve(d3.curveMonotoneX); + .curve(curveMonotoneX); g.append('path') .datum(combinedData) diff --git a/src/components/SpeciesComebackPanel.ts b/src/components/SpeciesComebackPanel.ts index 43c0d9ee5..1452c6bf2 100644 --- a/src/components/SpeciesComebackPanel.ts +++ b/src/components/SpeciesComebackPanel.ts @@ -8,7 +8,15 @@ */ import { Panel } from './Panel'; -import * as d3 from 'd3'; +import { + select, + extent, + max, + scaleLinear, + area, + curveMonotoneX, + line, +} from 'd3'; import type { SpeciesRecovery } from '@/services/conservation-data'; import { getCSSColor } from '@/utils'; import { replaceChildren } from '@/utils/dom-utils'; @@ -198,7 +206,7 @@ export class SpeciesComebackPanel extends Panel { const width = viewBoxWidth - SPARKLINE_MARGIN.left - SPARKLINE_MARGIN.right; const height = SPARKLINE_HEIGHT; - const svg = d3.select(container) + const svg = select(container) .append('svg') .attr('width', '100%') .attr('height', height + SPARKLINE_MARGIN.top + SPARKLINE_MARGIN.bottom) @@ -210,42 +218,42 @@ export class SpeciesComebackPanel extends Panel { .attr('transform', `translate(${SPARKLINE_MARGIN.left},${SPARKLINE_MARGIN.top})`); // Scales - const xExtent = d3.extent(data, d => d.year) as [number, number]; - const yMax = d3.max(data, d => d.value) as number; + const xExtent = extent(data, d => d.year) as [number, number]; + const yMax = max(data, d => d.value) as number; const yPadding = yMax * 0.1; - const x = d3.scaleLinear() + const x = scaleLinear() .domain(xExtent) .range([0, width]); - const y = d3.scaleLinear() + const y = scaleLinear() .domain([0, yMax + yPadding]) .range([height, 0]); // Area generator with smooth curve - const area = d3.area<{ year: number; value: number }>() + const areaGen = area<{ year: number; value: number }>() .x(d => x(d.year)) .y0(height) .y1(d => y(d.value)) - .curve(d3.curveMonotoneX); + .curve(curveMonotoneX); // Line generator for top edge - const line = d3.line<{ year: number; value: number }>() + const lineGen = line<{ year: number; value: number }>() .x(d => x(d.year)) .y(d => y(d.value)) - .curve(d3.curveMonotoneX); + .curve(curveMonotoneX); // Filled area g.append('path') .datum(data) - .attr('d', area) + .attr('d', areaGen) .attr('fill', color) .attr('opacity', 0.2); // Stroke line g.append('path') .datum(data) - .attr('d', line) + .attr('d', lineGen) .attr('fill', 'none') .attr('stroke', color) .attr('stroke-width', 1.5);