303 lines
10 KiB
Vue
303 lines
10 KiB
Vue
<script setup lang="ts">
|
|
import { computed, inject, provide, ref, toValue, watch } from 'vue';
|
|
import { AXIS_DATA, type AxisData, AxisType } from '@/graph/axis';
|
|
import { GRAPH_DATA, type GraphData, GraphSide } from '@/graph/graph';
|
|
import NumericText from '@/components/NumericText.vue';
|
|
|
|
const props = defineProps<{
|
|
y_limits?: [number, number];
|
|
side?: GraphSide;
|
|
type?: AxisType;
|
|
}>();
|
|
|
|
const minor_tick_length = computed(() => 4);
|
|
const major_tick_length = computed(() => 8);
|
|
const text_offset = computed(() => 5);
|
|
const side = computed(() => props.side || GraphSide.Right);
|
|
const type = computed(() => props.type || AxisType.Linear);
|
|
|
|
const ticker_locations = [Math.log10(1), Math.log10(2), Math.log10(5)];
|
|
ticker_locations.reverse();
|
|
|
|
const graph_data = inject<GraphData>(GRAPH_DATA)!;
|
|
|
|
const min_y = ref(Infinity);
|
|
const max_y = ref(-Infinity);
|
|
const raw_min_y = ref(Infinity);
|
|
const raw_max_y = ref(-Infinity);
|
|
|
|
const axis_update_watch = ref(0);
|
|
|
|
watch([graph_data.min_x, graph_data.max_x], () => {
|
|
axis_update_watch.value++;
|
|
min_y.value = raw_min_y.value;
|
|
max_y.value = raw_max_y.value;
|
|
raw_min_y.value = Infinity;
|
|
raw_max_y.value = -Infinity;
|
|
});
|
|
|
|
function update_min_y(y: number) {
|
|
if (y < min_y.value) {
|
|
min_y.value = y;
|
|
}
|
|
if (y < raw_min_y.value) {
|
|
raw_min_y.value = y;
|
|
}
|
|
}
|
|
function update_max_y(y: number) {
|
|
if (y > max_y.value) {
|
|
max_y.value = y;
|
|
}
|
|
if (y > raw_max_y.value) {
|
|
raw_max_y.value = y;
|
|
}
|
|
}
|
|
|
|
const min_y_value = computed(() => {
|
|
if (props.y_limits) {
|
|
return props.y_limits[0];
|
|
}
|
|
if (type.value == AxisType.Linear) {
|
|
if (max_y.value > min_y.value) {
|
|
const half_diff_y_value = (max_y.value - min_y.value) / 2.0;
|
|
const average_y_value = min_y.value + half_diff_y_value;
|
|
return average_y_value - half_diff_y_value * 1.05;
|
|
} else {
|
|
return -1.0;
|
|
}
|
|
} else {
|
|
if (max_y.value > min_y.value) {
|
|
const half_diff_y_value =
|
|
(Math.log(max_y.value) - Math.log(min_y.value)) / 2.0;
|
|
const average_y_value = Math.log(min_y.value) + half_diff_y_value;
|
|
return Math.exp(average_y_value - half_diff_y_value * 1.05);
|
|
} else {
|
|
return 0.0;
|
|
}
|
|
}
|
|
});
|
|
const max_y_value = computed(() => {
|
|
if (props.y_limits) {
|
|
return props.y_limits[1];
|
|
}
|
|
if (type.value == AxisType.Linear) {
|
|
if (max_y.value > min_y.value) {
|
|
const half_diff_y_value = (max_y.value - min_y.value) / 2.0;
|
|
const average_y_value = min_y.value + half_diff_y_value;
|
|
return average_y_value + half_diff_y_value * 1.05;
|
|
} else {
|
|
return 1.0;
|
|
}
|
|
} else {
|
|
if (max_y.value > min_y.value) {
|
|
const half_diff_y_value =
|
|
(Math.log(max_y.value) - Math.log(min_y.value)) / 2.0;
|
|
const average_y_value = Math.log(min_y.value) + half_diff_y_value;
|
|
return Math.exp(average_y_value + half_diff_y_value * 1.05);
|
|
} else {
|
|
return 1.0;
|
|
}
|
|
}
|
|
});
|
|
|
|
const y_map = (y: number) => {
|
|
const height = toValue(graph_data.height);
|
|
const border_top = toValue(graph_data.border_top);
|
|
let max_value = toValue(max_y_value);
|
|
let min_value = toValue(min_y_value);
|
|
let y_value = y;
|
|
if (type.value == AxisType.Logarithmic) {
|
|
max_value = Math.log(max_value);
|
|
min_value = Math.log(min_value);
|
|
y_value = Math.log(y_value);
|
|
}
|
|
const diff_y = max_value - min_value;
|
|
return height * (1 - (y_value - min_value) / diff_y) + border_top;
|
|
};
|
|
|
|
provide<AxisData>(AXIS_DATA, {
|
|
axis_update_watch: axis_update_watch,
|
|
min_y_callback: update_min_y,
|
|
max_y_callback: update_max_y,
|
|
y_map: y_map,
|
|
});
|
|
|
|
const lines = computed(() => {
|
|
const diff_y_val = (max_y_value.value - min_y_value.value) / 2;
|
|
const diff_log10 = Math.log10(diff_y_val);
|
|
const diff_log10_fraction = diff_log10 - Math.floor(diff_log10);
|
|
const ticker_location = ticker_locations.find(
|
|
(location) => location < diff_log10_fraction,
|
|
)!;
|
|
const grid_spread = Math.pow(10, Math.floor(diff_log10) + ticker_location);
|
|
const major_spread = grid_spread / 2;
|
|
const minor_spread = major_spread / 2;
|
|
const minor_ticks = [];
|
|
const major_ticks = [];
|
|
const grid_lines = [];
|
|
for (
|
|
let i = Math.floor(min_y_value.value / grid_spread);
|
|
i <= Math.ceil(max_y_value.value / grid_spread);
|
|
i++
|
|
) {
|
|
const y = i * grid_spread;
|
|
grid_lines.push(y);
|
|
}
|
|
for (
|
|
let i = Math.floor(min_y_value.value / major_spread);
|
|
i <= Math.ceil(max_y_value.value / major_spread);
|
|
i++
|
|
) {
|
|
const y = i * major_spread;
|
|
if (grid_lines.indexOf(y) < 0) {
|
|
major_ticks.push(y);
|
|
}
|
|
}
|
|
for (
|
|
let i = Math.floor(min_y_value.value / minor_spread);
|
|
i <= Math.ceil(max_y_value.value / minor_spread);
|
|
i++
|
|
) {
|
|
const y = i * minor_spread;
|
|
if (grid_lines.indexOf(y) < 0 && major_ticks.indexOf(y) < 0) {
|
|
minor_ticks.push(y);
|
|
}
|
|
}
|
|
return [minor_ticks, major_ticks, grid_lines];
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<template v-if="side == GraphSide.Right">
|
|
<g class="minor_tick" clip-path="url(#y_ticker)">
|
|
<template v-for="tick of lines[0]" :key="tick">
|
|
<polyline
|
|
:points="`${graph_data.x_map(toValue(graph_data.max_x)) - minor_tick_length},${y_map(tick)} ${graph_data.x_map(toValue(graph_data.max_x))},${y_map(tick)}`"
|
|
></polyline>
|
|
<text
|
|
class="right_edge middle_text"
|
|
:x="
|
|
graph_data.x_map(toValue(graph_data.max_x)) +
|
|
text_offset
|
|
"
|
|
:y="y_map(tick)"
|
|
><NumericText :value="tick" :max_width="7"></NumericText></text>
|
|
</template>
|
|
</g>
|
|
<g class="major_tick" clip-path="url(#y_ticker)">
|
|
<template v-for="tick of lines[1]" :key="tick">
|
|
<polyline
|
|
:points="`${graph_data.x_map(toValue(graph_data.max_x)) - major_tick_length},${y_map(tick)} ${graph_data.x_map(toValue(graph_data.max_x))},${y_map(tick)}`"
|
|
></polyline>
|
|
<text
|
|
class="right_edge middle_text"
|
|
:x="
|
|
graph_data.x_map(toValue(graph_data.max_x)) +
|
|
text_offset
|
|
"
|
|
:y="y_map(tick)"
|
|
><NumericText :value="tick" :max_width="6"></NumericText></text
|
|
>
|
|
</template>
|
|
</g>
|
|
<g class="grid_tick" clip-path="url(#y_ticker)">
|
|
<template v-for="tick of lines[2]" :key="tick">
|
|
<polyline
|
|
:points="`${graph_data.x_map(toValue(graph_data.min_x))},${y_map(tick)} ${graph_data.x_map(toValue(graph_data.max_x))},${y_map(tick)}`"
|
|
></polyline>
|
|
<text
|
|
class="right_edge middle_text"
|
|
:x="
|
|
graph_data.x_map(toValue(graph_data.max_x)) +
|
|
text_offset
|
|
"
|
|
:y="y_map(tick)"
|
|
>
|
|
<NumericText :value="tick" :max_width="5"></NumericText>
|
|
</text
|
|
>
|
|
</template>
|
|
</g>
|
|
</template>
|
|
<template v-if="side == GraphSide.Left">
|
|
<g class="minor_tick" clip-path="url(#y_ticker)">
|
|
<template v-for="tick of lines[0]" :key="tick">
|
|
<polyline
|
|
:points="`${graph_data.x_map(toValue(graph_data.min_x)) + minor_tick_length},${y_map(tick)} ${graph_data.x_map(toValue(graph_data.min_x))},${y_map(tick)}`"
|
|
></polyline>
|
|
<text
|
|
class="left_edge middle_text"
|
|
:x="
|
|
graph_data.x_map(toValue(graph_data.min_x)) -
|
|
text_offset
|
|
"
|
|
:y="y_map(tick)"
|
|
><NumericText :value="tick" :max_width="7"></NumericText></text>
|
|
</template>
|
|
</g>
|
|
<g class="major_tick" clip-path="url(#y_ticker)">
|
|
<template v-for="tick of lines[1]" :key="tick">
|
|
<polyline
|
|
:points="`${graph_data.x_map(toValue(graph_data.min_x)) + major_tick_length},${y_map(tick)} ${graph_data.x_map(toValue(graph_data.min_x))},${y_map(tick)}`"
|
|
></polyline>
|
|
<text
|
|
class="left_edge middle_text"
|
|
:x="
|
|
graph_data.x_map(toValue(graph_data.min_x)) -
|
|
text_offset
|
|
"
|
|
:y="y_map(tick)"
|
|
><NumericText :value="tick" :max_width="6"></NumericText></text>
|
|
</template>
|
|
</g>
|
|
<g class="grid_tick" clip-path="url(#y_ticker)">
|
|
<template v-for="tick of lines[2]" :key="tick">
|
|
<polyline
|
|
:points="`${graph_data.x_map(toValue(graph_data.min_x))},${y_map(tick)} ${graph_data.x_map(toValue(graph_data.max_x))},${y_map(tick)}`"
|
|
></polyline>
|
|
<text
|
|
class="left_edge middle_text"
|
|
:x="
|
|
graph_data.x_map(toValue(graph_data.min_x)) -
|
|
text_offset
|
|
"
|
|
:y="y_map(tick)"
|
|
><NumericText :value="tick" :max_width="5"></NumericText></text>
|
|
</template>
|
|
</g>
|
|
</template>
|
|
<slot></slot>
|
|
</template>
|
|
|
|
<style scoped lang="scss">
|
|
@use '@/assets/variables';
|
|
|
|
.right_edge {
|
|
text-anchor: start;
|
|
}
|
|
|
|
.left_edge {
|
|
text-anchor: end;
|
|
}
|
|
|
|
.middle_text {
|
|
dominant-baseline: middle;
|
|
font-family: variables.$text-font;
|
|
}
|
|
|
|
.minor_tick {
|
|
stroke: variables.$minor-tick;
|
|
fill: variables.$minor-tick;
|
|
}
|
|
|
|
.major_tick {
|
|
stroke: variables.$major-tick;
|
|
fill: variables.$major-tick;
|
|
}
|
|
|
|
.grid_tick {
|
|
fill: variables.$grid-line;
|
|
stroke: variables.$grid-line;
|
|
}
|
|
</style>
|