Размещение текстовых меток вокруг круговых диаграмм (Pie Charts)

Константинов Александр Для реализации графиков под iOS и MacOS многие программисты используют бесплатную открытую библиотеку CorePlot (http://code.google.com/p/core-plot/). Популярность этой библиотеки обусловлена возможностью использования ее в коммерческих проектах, ее открытостью и высокой активностью разработчиков, которые улучшают код библиотеки и добавляют в нее новый функционал.   Но в данной библиотеке есть ряд недостатков. Например, некрасивое расположение меток вокруг круговых диаграмм (Pie Charts): они перекрывают друг друга, если несколько фрагментов круговых диаграмм имеют малое значение и расположены близко друг к другу:
Поскольку CorePlot поставляется с исходным кодом, есть возможность исправить эту ситуацию. В связи с тем, что результат расположения меток не формализован (метки должны располагаться, не перекрывая друг друга, но не определено, как именно они должны располагаться), то и подходов к решению данной задачи может быть множество. Рассмотрим путь продвижения к подходу, решающего данную задачу, через рассмотрение отклоненных подходов. Самая простая идея – расположить метки не горизонтально, а под такими углами, чтобы они ложились на продолжения биссектрис углов фрагментов. Тогда круговая диаграмма приобретает вид «солнца» с «лучами»: «солнце» – сама круговая диаграмма, «лучи» – метки. Но после преобразования метки станут нечитабельными: нужно будет постоянно поворачивать голову. Следующая идея – располагать метки с определенным отступом от диаграммы. Но формула для расчета данного отступа не очевидна: отступ будет зависеть от расположения других меток и длины их текста. Также может получится такая ситуация, что отступ получится такой большой, что метка выйдет за границу области графика. Расчет отступа можно упростить, разбив область графика на строчки. При необходимости нарисовать метку она рисуется в ближайшей к фрагменту диаграммы строчке, если на данной строчке еще нет метки. Если данная строчка занята, то находится ближайшая незанятая строчка. В данном методе также есть технические недостатки: необходимо хранить признаки занятости для строк, что может нарушить стройность исходного кода библиотеки; есть вероятность, что свободная строчка найдется на таком далеком расстоянии от диаграммы, что метка окажется за пределами области графика. Можно попробовать настроить не угол меток и их отступ, а значения меток. Каждый фрагмент на графике имеет свое значение, которое графически выражается углом фрагмента. Значение метки равно значению фрагмента. Это та область, которая отводится для метки. Угол в полярной системе координат, на котором рисуется метка, можно вычислить как сумма значений предыдущих фрагментов плюс половина текущего фрагмента. Итак, если отказаться от условия, что сумма значений меток должна равняться определенному числу (2*Pi в радианах или 1, если значения нормализованы), то можно увеличивать значения меток до определенной величины, которая достаточна для отображения меток. Но плотность меток при одинаковых значениях различна в разных частях круговой диаграммы: вверху и внизу она больше, чем по бокам, придется увеличивать значения меток на большую величину. Также если многие значения меток будут увеличены, то в сумме значения дадут результат больший, чем 2*Pi или 1 (если значения нормализованы), следовательно, последние метки перекроют первые. Рабочая идея является синтезом двух идей: разбиение пространства графика на строчки и увеличение значений меток. Было замечено, что если выводить метки вертикально в столбец справа и слева от круговой диаграммы, то все они могут расположиться, не перекрывая друг друга и не выходя за пределы области графика, если их суммарная высота с каждой стороны не превышает высоту области графика. Если же данное условие не выполняется, то мы можем добавить на область графика прокручиваемые области (UIScrollView в терминологии iOS SDK), высота которых будет равна суммарной высоте меток с соответствующей стороны, что позволит разместить необходимое число меток. Ниже будет описан алгоритм размещения меток. Введем обозначения: currentWidth[i] – значение i-ой метки, startingWidth[i-1] – сумма значений меток от 0 – й до i-1 – й.   Пусть значения меток нормализованы: currentWidth[i] <= 1 и  = 1, где labelsCount – общее количество меток. labelIndent[i] = startingWidth[i-1] + currentWidth[i] / 2 – нормализованная координата метки (естественно, необходимо проверять i на равенство 0). Введем понятия «левой» и «правой» метки. «Правая» метка – метка, которая будет расположена справа от круговой диаграммы. «Правая» метка должна выполнять следующее условие: labelIndent[i] <= 0.5. Аналогично для «левой» метки, только условие должно быть: labelIndent[i] > 0.5. Перекрытие меток может произойти тогда, когда currentWidth[i] для какой-нибудь метки будет меньше такого порогового значения, при котором данной метке хватает достаточно пространства для отображения без наложения на другие метки. Высчитаем данное пороговое значение: minCurrentWidth[i] = labelHeight / (2*height), где labelHeight – высота метки (в пикселах), height – высота области графика (в пикселах); двойная высота обусловлена тем, что метки рисуются слева и справа. Если для метки, удовлетворяющей условию currentWidth[i] < minCurrentWidth[i], увеличить currentWidth[i] до порогового значения minCurrentWidth[i], то, поскольку условие   = 1 обязательно, придется уменьшить значение другой метки на ту же величину. Это может привести к неверному отображению меток. Например, пусть для i-ой метки выполняется условие labelIndent[i] > 0.5. Пусть значение k-ой метки увеличили до порогового значения, а значение i-ой метки уменьшили на соответствующую величину. Тогда для i-ой метки может не выполниться условие labelIndent[i] > 0.5, что повлечет за собой некорректное вычисление ее координаты, если данную метку оставить, как «левую». Перенос в правую сторону исключен, т.к. признаки определяются именно на основании первоначальных значений, которые совпадают со значениями фрагментов круговой диаграммы. Иначе соединительная линия метки, которая оказалась бы справа, шла бы налево до центра фрагмента круговой диаграммы и пересекала бы график, что выглядело бы некрасиво. Именно для исключения данной ситуации и было принято решение разделить метки на «левые» и «правые», которым необходимо задать независимое поведение от поведения меток из другой группы. Для удобства было создано 2 массива: leftCurrentWidth и rightCurrentWidth. В эти массивы были скопированы значения «левых» и «правых» меток. (Первый элемент в rightCurrentWidth – первая метка, первый элемент в leftCurrentWidth – последняя метка.) Чтобы выполнялось условие, что в rightCurrentWidth хранятся значения «правых» меток, а в leftCurrentWidth хранятся значения «левых» меток, необходимо выполнение следующих неравенств соответственно: + rightCurrentWidth[rightLabelsCount – 1] / 2 <= 0.5 и 1 – ( + leftCurrentWidth[leftLabelsCount – 1] / 2) > 0.5 Данные математические неравенства означают проверку: не стала ли последняя «правая» метка «левой», а первая «левая» не стала ли «правой» в терминах задачи. Основной алгоритм состоит в обработке массивов leftCurrentWidth и rightCurrentWidth. Рассмотрим для простоты работу с rightCurrentWidth, поскольку работа с leftCurrentWidth аналогична:
Цикл (по всем элементам массива) { Добавочное значение = разность между пороговым значением и текущим значением метки; Если (добавочное значение больше 0), то { Если (текущий элемент не последний в массиве (т.е. не последний для «правых» меток)), то { Если (к пропорции метки прибавить добавочное значение и полученная сумма <= 0.5) { Прибавляем к значению текущей метки добавочное значение; } иначе { Находим максимальный элемент массива, не учитывая последний; Если (данное максимальное значение не меньше суммы порога и добавочного значения), то { Отнимаем у данного максимального значения добавочное; Прибавляем к значению текущей метки добавочное значение; } иначе { Если (значение последнего элемента (последней «правой метки») не меньше суммы порога и добавочного значения, умноженного на 2), то { Отнимаем у последнего элемента добавочное значение, умноженное на 2; Прибавляем к значению текущей метки добавочное значение; } } } } иначе                         // текущий элемент последний в массиве (т.е. //последний для «правых» меток)<br /> { Если (к пропорции метки прибавить добавочное значение, деленное на 2, и полученная сумма <= 0.5), то { Прибавляем к значению текущей метки добавочное значение; } иначе { Находим максимальный элемент массива, не учитывая последний; Если (данное максимальное значение не меньше суммы порога и добавочного значения, деленного на 2), то { Отнимаем у данного максимального значения добавочное, деленное на 2; Прибавляем к значению текущей метки добавочное значение; } } } } }
В итоге получились 2 массива, в которых хранятся значения меток, большие порогового значения, которое нужно для расположения метки без перекрытия ее другими метками. Последняя «правая» метка не стала «левой», а первая «левая» метка на стала «правой» благодаря изменениям, которые делались только тогда, когда они были возможны, что обеспечило корректное расположение меток в пределах области графика. Проверка возможности тех или иных изменений осуществлялась с помощью проверки неравенств. После реализации данного алгоритма внутри библиотеки CorePlot получился ожидаемый результат: Хотите использовать наш опыт в разработке мобильных приложений для iPhone? Начните с оценки Вашего проекта прямо сейчас!