Skip to content

Commit

Permalink
Updated legends for the distribution track
Browse files Browse the repository at this point in the history
  • Loading branch information
tomktjemsland committed Jun 18, 2024
1 parent 905d666 commit 8f6c45f
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 75 deletions.
8 changes: 4 additions & 4 deletions examples/storybook/stories/shared/tracks.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ import {
} from './mock-data';

const distributionComponents = {
'carbonate': { color: 'LightSalmon' },
'sand': { color: 'Moccasin' },
'shale': { color: 'LightGray' },
'carbonate': { color: 'FireBrick' },
'sand': { color: 'BurlyWood' },
'shale': { color: 'LightSlateGray' },
};

export default (delayLoading = false) => {
Expand Down Expand Up @@ -136,7 +136,7 @@ export default (delayLoading = false) => {
label: 'Distribution',
abbr: 'Dst',
data: exampleDistributionData,
legendConfig: distributionLegendConfig(distributionComponents),
legendConfig: distributionLegendConfig,
components: distributionComponents,
interpolate: true,
}),
Expand Down
134 changes: 69 additions & 65 deletions src/tracks/distribution/distribution-legend.ts
Original file line number Diff line number Diff line change
@@ -1,92 +1,96 @@
import { select } from 'd3-selection';
import { clamp, lerp } from '@equinor/videx-math';
import LegendHelper, {
LegendBounds,
LegendConfig,
LegendOnUpdateFunction,
} from '../../utils/legend-helper';
import { D3Selection } from '../../common/interfaces';
import { DistributionComponents } from './interfaces';
import { DistributionTrackOptions } from './interfaces';
import { DistributionTrack } from './distribution-track';

const Padding = 20;
const LabelPadding = 10;
const MarginScale = 0.04;
const PaddingScale = 0.06;

function renderDistributionLabels(g: D3Selection, bounds: LegendBounds, data: DistributionComponents, horizontal: boolean = false) : void {
// Helper function for creating rect
const applyRectDimensions = (node: D3Selection, { x, y, width, height }, isHorizontal: boolean) => (
isHorizontal
? node.attr('x', y).attr('y', x).attr('width', height).attr('height', width)
: node.attr('x', x).attr('y', y).attr('width', width).attr('height', height)
);

function renderDistributionPlotLegend(g: D3Selection, bounds: LegendBounds, options: DistributionTrackOptions) : void {
const { width, height, left = 0, top = 0 } = bounds;
const { horizontal = false } = options;

// Get components in distribution, return if missing
const components = data && Object.entries(data);
if (!components) return;
const components = Object.entries(options.components ?? {});
if (components.length === 0) return;

const textSize = Math.min(12, width / 3);
const squareSize = textSize * 0.75;
const margin = Math.min(width, height) * MarginScale;
const padding = Math.min(width, height) * PaddingScale;

// Find width-span and height to draw labels along. These are relative to track orientation, not screen orientation.
const wMin = left + Padding;
const wMax = width - Padding;
const h = top + height - squareSize;
const componentStride = height / components.length;
const componentHeight = componentStride - margin;
const componentWidth = width - margin * 2;
const textSize = componentHeight - padding * 2;

// Calculate how many labels we can stack
const labelSpace = width - Padding * 2;
const labelCount = clamp(Math.floor(labelSpace / (textSize + LabelPadding)), 1, components.length);
const textX = left + width / 2;

for (let i = 0; i < labelCount; i++) {
const [label, component] = components[i];
components.forEach(([label, component], index) => {
const color = component.color;
const y = top + index * componentStride + margin / 2;
const textY = y + componentHeight / 2;

// Center a single label; distribute multiple labels evenly.
const t = labelCount === 1 ? 0.5 : i / (labelCount - 1);

// Define x and y for label. This is where the colored square is drawn
let x: number, y: number;
if (horizontal) {
y = lerp(wMin, wMax, t);
x = h;
} else {
x = lerp(wMin, wMax, t);
y = h;
}

// Append colored square
g.append('rect')
.attr('x', x - squareSize * 0.5)
.attr('y', y - squareSize * 0.5)
.attr('width', squareSize)
.attr('height', squareSize)
.attr('fill', color);
applyRectDimensions(
g.append('rect'),
{
x: left + margin,
y,
width: componentWidth,
height: componentHeight,
},
horizontal,
).attr('fill', color);

// Labels are displayed behind the square
const transform = horizontal
? `translate(${x - squareSize},${y})`
: `translate(${x},${y - squareSize})rotate(90)`;
? `translate(${textY},${textX})rotate(-90)`
: `translate(${textX},${textY})`;

// Append text
const lbl = g.append('text')
.attr('transform', transform)
.attr('font-size', `${textSize}px`)
.attr('dominant-baseline', 'middle')
.style('text-anchor', 'end');
lbl.text(label);
.style('text-anchor', 'middle')
.style('font-weight', 'bold')
.attr('fill', color)
.text(label)
.node();

// TODO: Show short form?
/*
const bbox = lbl.node().getBBox();
if (bbox.width > height * 0.8) {
lbl.text(label);
}
*/
}
const bbox = lbl.getBBox();
applyRectDimensions(
g.insert('rect', () => lbl),
{
x: textX - bbox.width / 2 - padding,
y: y + padding / 2,
width: bbox.width + padding * 2,
height: bbox.height + padding / 4,
},
horizontal,
).attr('fill', 'white');
});
}

export function distributionLegendConfig(data: DistributionComponents) : LegendConfig {
const onLegendUpdate: LegendOnUpdateFunction = (elm, bounds, track) => {
const g = select(elm);
g.selectAll('*').remove();
renderDistributionLabels(
g,
bounds,
data,
track.options.horizontal,
);
};
return LegendHelper.basicLegendSvgConfig(() => 3, onLegendUpdate);
function onUpdateLegend(elm: HTMLElement, bounds: LegendBounds, track: DistributionTrack): void {
const g = select(elm);
g.selectAll('*').remove();
renderDistributionPlotLegend(
g,
bounds,
track.options,
);
}

function getGraphTrackLegendRows(track: DistributionTrack): number {
return Object.keys(track.options?.components ?? {}).length || 3;
}

export default LegendHelper.basicLegendSvgConfig(getGraphTrackLegendRows, onUpdateLegend);
8 changes: 7 additions & 1 deletion src/tracks/distribution/distribution-track.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,10 @@ export class DistributionTrack extends CanvasTrack<DistributionTrackOptions> {
const [min, max] = yscale.domain();

// Filter data based on the visible domain
const visibleData = data.filter((d: DistributionData) => d.depth + discreteHeight >= min && d.depth - discreteHeight <= max);
const visibleData = data.filter((d: DistributionData) => d && d.depth + discreteHeight >= min && d.depth - discreteHeight <= max);

// Return if no visible data
if (!visibleData?.length) return;

// Transform depth
const transformedData: DistributionData[] = visibleData.map((d: DistributionData) => ({
Expand Down Expand Up @@ -163,6 +166,9 @@ export class DistributionTrack extends CanvasTrack<DistributionTrackOptions> {
return (d.depth >= min && d.depth <= max) || nextDepth > min || prevDepth < max;
});

// Return if no visible data
if (!visibleData?.length) return;

// Initiate distribution polygons
const polygonData: { [key: string]: DistributionPolygon } = {};
Object.entries(options.components).forEach(([key, component]) => {
Expand Down
2 changes: 1 addition & 1 deletion src/tracks/distribution/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { DistributionTrack } from './distribution-track';
export { distributionLegendConfig } from './distribution-legend';
export { default as distributionLegendConfig } from './distribution-legend';
16 changes: 12 additions & 4 deletions src/tracks/graph/graph-legend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,19 @@ function onUpdateLegend(elm: HTMLElement, bounds: LegendBounds, track: GraphTrac
const lg = select(elm);

const g = lg.select('.svg-legend');
const rows = g.selectAll('.legend-row').data(track.plots);
rows.enter().append('g').classed('legend-row', true);
rows.exit().remove();

g.selectAll('.legend-row').call(updateLegendRows, bounds, track);
const rows = g.selectAll('.legend-row')
.data(track.plots)
.call(updateLegendRows, bounds, track);

// Add new rows
rows.enter()
.append('g')
.classed('legend-row', true)
.call(updateLegendRows, bounds, track);

// Remove rows without data
rows.exit().remove();
}

/**
Expand Down

0 comments on commit 8f6c45f

Please sign in to comment.