Позвольте мне объяснить, что я пытаюсь выполнить: я хочу нарисовать диаграмму даты и времени, используя библиотеки d3, что позволяет мне рисовать как серию, так и ось.
Теперь svg, который содержит осьи серия должна быть полностью изменяемого размера, поэтому диаграмма в ней должна соответствовать изменению размера контейнера.Более того, существует ограничение, состоящее в том, что отметки на оси x имеют минимальную ширину.
Теперь, в чем проблема ... Идея / алгоритм, который я реализую, основан на двух простых моментах:
- В состоянии компонента я сохраняю размер svg и маржу, которая требуется для графика, по осям (если у меня есть метка тика в начале или в конце графика, если я не хочу, чтобы они были скрыты, мне нужен запас!)
- SVG берет 100% своего родителя, но ряд и скалярная функция для оси x вычисляются на
svg size - margin sizes
Итак, позвольте мне показать вам пример, который также привел к проблеме (это цикл, который приводит к сбою приложения), предполагая, что каждый тик должен иметь длину не менее 50px
:
Компонент имеет chartWidth
, равный 500px
, а leftMargin
и rightMargin
оба установлены на 0
.Скалярная функция вычисляется на chartWidth - (leftMargin + rightMargin)
.Таким образом, мы можем иметь 500 / 50 = 10
тик.
Тики вводятся в график, НО!Один из тиков находится прямо в начале оси X, поэтому для его скрытия требуется поле (при условии 5px
), чтобы не скрывать → состояние компонента обновляется так, что leftMargin = 5
, rightMargin = 5
,и, таким образом, chartWidth = 490
.
Поскольку состояние было обновлено, скалярная функция и метка вычисляются снова: у нас есть chartWidth = 490
, поэтому у нас есть 490 / 9 = 9
меток. ЕСТЬ ПРОБЛЕМА!
Тики вставляются в график, но теперь в начале графика нет метки. Итак, leftMargin
и rightMargin
установлены на 0
м и, таким образом, chartWidth
снова 500
Угадайте, что?Мы вернулись к шагу (1), и цикл никогда не заканчивается> _>
Я создал фрагмент, чтобы увидеть проблему, которую также можно увидеть здесь -> https://codepen.io/Gesma94/pen/vbMLrN
Если вы измените размер контейнера, вы увидите, что приложение вылетает (уменьшите его, оно очень скоро достигнет критической точки).
Самое простое решение - учитывать поля, когда я вычисляюномер тика.Таким образом, вместо:
const tickNumber = Math.floor(domainWidth / MINIMUM_X_TICK);
я бы использовал:
const tickNumber = Math.floor((chartWidth + (leftMargin*2) / MINIMUM_X_TICK);
Но, делая так, я потерял реальное MINIMUM_X_TICK
, поскольку я рассматриваю не chartWidth
, а containerWidth
.
Более того, если я добавлю ось Y, ошибки могут возрасти, поскольку у меня будет две переменные (поля, заданные как xось и ось Y).
Итак, в отличие от ответа (я думаю), мой вопрос довольно прост: есть ли простой способ решить эту проблему?Может быть, уже есть «как», которого я не нашел (я знаю «соглашение о запасах» → https://bl.ocks.org/mbostock/3019563,, но мне нужна его «лучшая» версия).
У меня естьборолись уже несколько дней, и у меня заканчиваются идеи> _>
const MINIMUM_X_TICK = 50;
const startDate = "2017-09-10T09:21:00.002Z";
const endDate = "2017-09-30T15:43:00.081Z";
class App extends React.Component {
xAxisRef = React.createRef();
yAxisRef = React.createRef();
containerRef = React.createRef();
counter = 0;
resizeObserver;
constructor(props) {
super(props);
this.state = {
leftMargin: 0,
rightMargin: 0,
topMargin: 0,
bottomMargn: 0,
chartWidth: 250,
charHeight: 185,
containerWidth: 250,
containerHeight: 185
}
this.resizeObserver = new ResizeObserver(entries => {
for (const entry of entries) {
if(entry && entry.contentRect) {
this.onResize(entry.contentRect.width, entry.contentRect.height);
}
}
});
}
onResize = (width, height) => {
console.log("width is: " + width);
this.setState(prevState => ({
containerWidth: width,
chartWidth: width - (prevState.leftMargin*2),
containerHeight: height - 15
}));
};
componentDidMount() {
this.resizeObserver.observe(this.containerRef.current);
this.injectXAxis();
};
componentDidUpdate() {
this.injectXAxis();
this.adjustMargin();
}
injectXAxis() {
const {leftMargin, rightMargin, chartWidth} = this.state;
const xAxisDOM = this.xAxisRef.current;
const xScale = d3.scaleTime()
.domain([new Date(startDate), new Date(endDate)])
.range([leftMargin, chartWidth + leftMargin]);
const timeSpan = (new Date(endDate).getTime()) - (new Date(startDate).getTime());
const msPerPixel = timeSpan / (chartWidth);
const every = Math.ceil((msPerPixel * MINIMUM_X_TICK) / 86400000);
const label = d3.timeDay.every(every).range(new Date(startDate), new Date(endDate));
const xAxis = d3.axisBottom(xScale).tickValues(label);
xAxis(d3.select(xAxisDOM));
}
adjustMargin() {
const {leftMargin, rightMargin, chartWidth} = this.state;
const xAxisDOM = this.xAxisRef.current;
const xDomainDOM = d3.select(xAxisDOM).select('.domain').node();
const xAxisBR = xAxisDOM.getBoundingClientRect();
const xDomainBR = xDomainDOM.getBoundingClientRect();
const leftMarginRequired = xDomainBR.left - xAxisBR.left;
const rightMarginRequired = xAxisBR.right - xDomainBR.right;
const marginRequired = Math.ceil(d3.max([leftMarginRequired, rightMarginRequired]));
if (marginRequired !== leftMargin || marginRequired !== rightMargin) {
this.setState(prevState => ({
leftMargin: marginRequired,
rightMargin: marginRequired,
chartWidth: prevState.containerWidth - (marginRequired*2)
}));
}
}
render() {
console.log(JSON.stringify(this.state, null,4));
return (
<div className="resizer">
<div className="svgContainer" ref={this.containerRef}>
<svg width={this.state.chartWidth} height={this.state.chartHeight}>
<g data-content="x-axis" ref={this.xAxisRef} />
<g data-content="y-axis" ref={this.yAxisRef} />
</svg>
</div>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
@import url(https://fonts.googleapis.com/css?family=Montserrat);
body {
font-family: 'Montserrat', sans-serif;
}
.svgContainer {
width: 100%;
height: calc(100% - 15px);
}
.resizer {
width: 250px;
height: 200px;
resize: both;
overflow: hidden;
background: white;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.2/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.9.1/d3.min.js"></script>
<body>
<div id='root'></div>
</body>