diff --git a/src/components/MultiLink.vue b/src/components/MultiLink.vue
index 56535c01..a2f65e20 100644
--- a/src/components/MultiLink.vue
+++ b/src/components/MultiLink.vue
@@ -10,7 +10,7 @@ import { select } from 'd3-selection';
import store from '@/store';
import {
- Node, Edge, SimulationEdge,
+ Node, Edge, SimulationEdge, AttributeRange,
} from '@/types';
import ContextMenu from '@/components/ContextMenu.vue';
@@ -20,6 +20,7 @@ import {
} from '@vue/composition-api';
import { axisBottom, axisLeft } from 'd3-axis';
import { isInternalField } from '@/lib/typeUtils';
+import { ColumnType } from 'multinet';
export default defineComponent({
components: {
@@ -67,6 +68,7 @@ export default defineComponent({
const controlsWidth = computed(() => store.state.controlsWidth);
const directionalEdges = computed(() => store.state.directionalEdges);
const edgeColorScale = computed(() => store.getters.edgeColorScale);
+ const clipRegionSize = 100;
// Update height and width as the window size changes
// Also update center attraction forces as the size changes
@@ -591,29 +593,163 @@ export default defineComponent({
}
});
+ const xAxisPadding = 60;
+ const yAxisPadding = 80;
const layoutVars = computed(() => store.state.layoutVars);
- watch(layoutVars, () => {
+ function makePositionScale(axis: 'x' | 'y', type: ColumnType, range: AttributeRange) {
+ const varName = layoutVars.value[axis];
+ let clipLow = false;
+ let clipHigh = false;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ let positionScale: any;
+
+ if (type === 'number') {
+ let minValue = range.min;
+ let maxValue = range.max - 1; // subtract 1, because of the + 1 on the legend chart scale
+
+ // Check IQR for outliers
+ if (network.value !== null && varName !== null) {
+ const values = network.value.nodes.map((node) => node[varName]).sort((a, b) => a - b);
+
+ let q1;
+ let q3;
+ if ((values.length / 4) % 1 === 0) {
+ q1 = 0.5 * (values[(values.length / 4)] + values[(values.length / 4) + 1]);
+ q3 = 0.5 * (values[(values.length * (3 / 4))] + values[(values.length * (3 / 4)) + 1]);
+ } else {
+ q1 = values[Math.floor(values.length / 4 + 1)];
+ q3 = values[Math.ceil(values.length * (3 / 4) + 1)];
+ }
+
+ const iqr = q3 - q1;
+ const maxCandidate = q3 + iqr * 1.5;
+ const minCandidate = q1 - iqr * 1.5;
+
+ if (maxCandidate < maxValue) {
+ maxValue = maxCandidate;
+ clipHigh = true;
+ select(`#${axis}-high-clip`).style('visibility', 'visible');
+ select(`#${axis}-high-clip > text`).text(`> ${maxCandidate}`);
+ }
+
+ if (minCandidate > minValue) {
+ minValue = minCandidate;
+ clipLow = true;
+ select(`#${axis}-low-clip`).style('visibility', 'visible');
+ select(`#${axis}-low-clip > text`).text(`< ${minCandidate}`);
+ }
+ }
+
+ positionScale = scaleLinear()
+ .domain([minValue, maxValue]);
+ } else {
+ positionScale = scaleBand()
+ .domain(range.binLabels);
+ }
+
+ if (axis === 'x') {
+ const minMax = [clipLow ? yAxisPadding + clipRegionSize : yAxisPadding, clipHigh ? store.state.svgDimensions.width - clipRegionSize : store.state.svgDimensions.width];
+ positionScale = positionScale
+ .range(minMax);
+ } else {
+ const minMax = [clipLow ? store.state.svgDimensions.height - xAxisPadding - clipRegionSize : store.state.svgDimensions.height - xAxisPadding, clipHigh ? clipRegionSize : 0];
+ positionScale = positionScale
+ .range(minMax);
+ }
+
+ const otherAxis = axis === 'x' ? 'y' : 'x';
+
+ if (varName !== null) {
+ // Set node size smaller
+ store.commit.setMarkerSize({ markerSize: 10, updateProv: true });
+
+ // Clear the label variable
+ store.commit.setLabelVariable(undefined);
+
+ store.commit.stopSimulation();
+
+ if (store.state.network !== null && store.state.columnTypes !== null) {
+ const otherAxisPadding = axis === 'x' ? 80 : 60;
+
+ if (type === 'number') {
+ const scaleDomain = positionScale.domain();
+ const scaleRange = positionScale.range();
+ store.state.network.nodes.forEach((node) => {
+ const nodeVal = node[varName];
+ let position = positionScale(nodeVal);
+
+ if (axis === 'x') {
+ position = nodeVal > scaleDomain[1] ? scaleRange[1] + ((clipRegionSize - 10) * ((nodeVal - scaleDomain[1]) / (range.max - 1 - scaleDomain[1]))) : position;
+ position = nodeVal < scaleDomain[0] ? scaleRange[0] - ((clipRegionSize - 10) * ((scaleDomain[0] - nodeVal) / (scaleDomain[0] - range.min))) : position;
+ } else {
+ position = nodeVal > scaleDomain[1] ? scaleRange[1] - ((clipRegionSize - 10) * ((nodeVal - scaleDomain[1]) / (range.max - 1 - scaleDomain[1]))) : position;
+ position = nodeVal < scaleDomain[0] ? scaleRange[0] + ((clipRegionSize - 10) * ((scaleDomain[0] - nodeVal) / (scaleDomain[0] - range.min))) : position;
+ }
+ position -= (markerSize.value / 2);
+
+ // eslint-disable-next-line no-param-reassign
+ node[axis] = position;
+ // eslint-disable-next-line no-param-reassign
+ node[`f${axis}`] = position;
+
+ if (store.state.layoutVars[otherAxis] === null) {
+ const otherSvgDimension = axis === 'x' ? store.state.svgDimensions.height : store.state.svgDimensions.width;
+ // eslint-disable-next-line no-param-reassign
+ node[otherAxis] = otherSvgDimension / 2;
+ // eslint-disable-next-line no-param-reassign
+ node[`f${otherAxis}`] = otherSvgDimension / 2;
+ }
+ });
+ } else {
+ let positionOffset: number;
+
+ if (axis === 'x') {
+ positionOffset = (store.state.svgDimensions.width - otherAxisPadding) / ((range.binLabels.length) * 2);
+ } else {
+ positionOffset = (store.state.svgDimensions.height - xAxisPadding - 10) / ((range.binLabels.length) * 2);
+ }
+
+ store.state.network.nodes.forEach((node) => {
+ // eslint-disable-next-line no-param-reassign
+ node[axis] = (positionScale(node[varName]) || 0) + positionOffset;
+ // eslint-disable-next-line no-param-reassign
+ node[`f${axis}`] = (positionScale(node[varName]) || 0) + positionOffset;
+
+ if (store.state.layoutVars[otherAxis] === null) {
+ const otherSvgDimension = axis === 'x' ? store.state.svgDimensions.height : store.state.svgDimensions.width;
+ // eslint-disable-next-line no-param-reassign
+ node[otherAxis] = otherSvgDimension / 2;
+ // eslint-disable-next-line no-param-reassign
+ node[`f${otherAxis}`] = otherSvgDimension / 2;
+ }
+ });
+ }
+ }
+ } else if (store.state.layoutVars[otherAxis] === null) {
+ store.dispatch.releaseNodes();
+ }
+
+ return positionScale;
+ }
+
+ function resetAxesClipRegions() {
select('#axes').selectAll('g').remove();
- const xAxisPadding = 60;
- const yAxisPadding = 80;
+
+ select('#x-low-clip').style('visibility', 'hidden');
+ select('#x-high-clip').style('visibility', 'hidden');
+ select('#y-low-clip').style('visibility', 'hidden');
+ select('#y-high-clip').style('visibility', 'hidden');
+ }
+
+ watch(layoutVars, () => {
+ resetAxesClipRegions();
// Add x layout
if (store.state.columnTypes !== null && layoutVars.value.x !== null) {
const type = store.state.columnTypes[layoutVars.value.x];
const range = store.state.attributeRanges[layoutVars.value.x];
- const maxPosition = store.state.svgDimensions.width - 10;
-
- let positionScale;
-
- if (type === 'number') {
- positionScale = scaleLinear()
- .domain([range.min, range.max])
- .range([yAxisPadding, maxPosition]);
- } else {
- positionScale = scaleBand()
- .domain(range.binLabels)
- .range([yAxisPadding, maxPosition]);
- }
+
+ const positionScale = makePositionScale('x', type, range);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const xAxis = axisBottom(positionScale as any);
@@ -635,7 +771,7 @@ export default defineComponent({
.attr('fill', 'currentColor')
.attr('font-size', '14px')
.attr('font-weight', 'bold')
- .attr('x', ((maxPosition - yAxisPadding) / 2) + yAxisPadding)
+ .attr('x', ((store.state.svgDimensions.width - yAxisPadding) / 2) + yAxisPadding)
.attr('y', xAxisPadding - 20);
const labelRectPos = (label.node() as SVGTextElement).getBBox();
@@ -652,19 +788,8 @@ export default defineComponent({
if (store.state.columnTypes !== null && layoutVars.value.y !== null) {
const type = store.state.columnTypes[layoutVars.value.y];
const range = store.state.attributeRanges[layoutVars.value.y];
- const maxPosition = store.state.svgDimensions.height - xAxisPadding;
-
- let positionScale;
-
- if (type === 'number') {
- positionScale = scaleLinear()
- .domain([range.min, range.max])
- .range([maxPosition, 10]);
- } else {
- positionScale = scaleBand()
- .domain(range.binLabels)
- .range([maxPosition, 10]);
- }
+
+ const positionScale = makePositionScale('y', type, range);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const yAxis = axisLeft(positionScale as any);
@@ -688,7 +813,7 @@ export default defineComponent({
.attr('font-size', '14px')
.attr('font-weight', 'bold')
.attr('text-anchor', 'middle')
- .attr('x', ((maxPosition - 10) / 2) + 10)
+ .attr('x', ((store.state.svgDimensions.height - xAxisPadding - 10) / 2) + 10)
.attr('y', yAxisPadding - 20);
const labelRectPos = (label.node() as SVGTextElement).getBBox();
@@ -734,6 +859,9 @@ export default defineComponent({
nestedPadding,
nodeBarColorScale,
glyphFill,
+ clipRegionSize,
+ xAxisPadding,
+ yAxisPadding,
};
},
});
@@ -763,6 +891,85 @@ export default defineComponent({
+
+
+
+
+ low values
+
+
+
+
+
+ high values
+
+
+
+
+
+ low values
+
+
+
+
+
+ high values
+
+
+
+
diff --git a/src/store/index.ts b/src/store/index.ts
index 74e317c2..7ee2d7ab 100644
--- a/src/store/index.ts
+++ b/src/store/index.ts
@@ -11,7 +11,7 @@ import {
import api from '@/api';
import { ColumnTypes, NetworkSpec, UserSpec } from 'multinet';
import {
- ScaleBand, scaleBand, ScaleLinear, scaleLinear, scaleOrdinal, scaleSequential,
+ scaleLinear, scaleOrdinal, scaleSequential,
} from 'd3-scale';
import { interpolateBlues, interpolateReds, schemeCategory10 } from 'd3-scale-chromatic';
import { initProvenance, Provenance } from '@visdesignlab/trrack';
@@ -560,84 +560,6 @@ const {
} = payload;
const otherAxis = axis === 'x' ? 'y' : 'x';
- if (varName !== null) {
- // Set node size smaller
- commit.setMarkerSize({ markerSize: 10, updateProv: true });
-
- // Clear the label variable
- commit.setLabelVariable(undefined);
-
- commit.stopSimulation();
-
- if (state.network !== null && state.columnTypes !== null) {
- const type = state.columnTypes[varName];
- const range = state.attributeRanges[varName];
- const otherAxisPadding = axis === 'x' ? 80 : 60;
- const maxPosition = axis === 'x' ? state.svgDimensions.width - 10 : state.svgDimensions.height - otherAxisPadding - state.markerSize;
-
- if (type === 'number') {
- let positionScale: ScaleLinear;
-
- if (axis === 'x') {
- positionScale = scaleLinear()
- .domain([range.min, range.max])
- .range([otherAxisPadding, maxPosition]);
- } else {
- positionScale = scaleLinear()
- .domain([range.min, range.max])
- .range([maxPosition, 10]);
- }
-
- state.network.nodes.forEach((node) => {
- // eslint-disable-next-line no-param-reassign
- node[axis] = positionScale(node[varName]);
- // eslint-disable-next-line no-param-reassign
- node[`f${axis}`] = positionScale(node[varName]);
-
- if (state.layoutVars[otherAxis] === null) {
- const otherSvgDimension = axis === 'x' ? state.svgDimensions.height : state.svgDimensions.width;
- // eslint-disable-next-line no-param-reassign
- node[otherAxis] = otherSvgDimension / 2;
- // eslint-disable-next-line no-param-reassign
- node[`f${otherAxis}`] = otherSvgDimension / 2;
- }
- });
- } else {
- let positionScale: ScaleBand;
- let positionOffset: number;
-
- if (axis === 'x') {
- positionScale = scaleBand()
- .domain(range.binLabels)
- .range([otherAxisPadding, maxPosition]);
- positionOffset = (maxPosition - otherAxisPadding) / ((range.binLabels.length) * 2);
- } else {
- positionScale = scaleBand()
- .domain(range.binLabels)
- .range([maxPosition, 10]);
- positionOffset = (maxPosition - 10) / ((range.binLabels.length) * 2);
- }
-
- state.network.nodes.forEach((node) => {
- // eslint-disable-next-line no-param-reassign
- node[axis] = (positionScale(node[varName]) || 0) + positionOffset;
- // eslint-disable-next-line no-param-reassign
- node[`f${axis}`] = (positionScale(node[varName]) || 0) + positionOffset;
-
- if (state.layoutVars[otherAxis] === null) {
- const otherSvgDimension = axis === 'x' ? state.svgDimensions.height : state.svgDimensions.width;
- // eslint-disable-next-line no-param-reassign
- node[otherAxis] = otherSvgDimension / 2;
- // eslint-disable-next-line no-param-reassign
- node[`f${otherAxis}`] = otherSvgDimension / 2;
- }
- });
- }
- }
- } else if (state.layoutVars[otherAxis] === null) {
- dispatch.releaseNodes();
- }
-
const updatedLayoutVars = { [axis]: varName, [otherAxis]: state.layoutVars[otherAxis] } as {
x: string | null;
y: string | null;
@@ -650,6 +572,9 @@ const {
commit.setMarkerSize({ markerSize: 11, updateProv: false });
dispatch.applyVariableLayout({ varName: state.layoutVars[otherAxis], axis: otherAxis });
+ } else if (varName === null && state.layoutVars[otherAxis] === null) {
+ // If both null, release
+ dispatch.releaseNodes();
}
},
},