Статья будет полезна web-разработчикам, которые задумались об отображении математических формул в браузере, ну и, наверное, другим IT-ам для общего развития.
У нас в компании уже давно внедрена система стимулирования сотрудников (KPI) на базе «Redmine», совмещающая функции расчета ЗП. Расскажу о ней в двух словах.
У каждой должности есть какой-то оклад, сотрудник, занимающий должность, может славно поработать и умножить свой оклад на коэффициент результативности, который вычисляется на основе кучки показателей настроенных для его должности. Коэффициент результативности может быть как больше, так и меньше единицы. Таким образом, моделируя показатели, можно стимулировать сотрудников определенных должностей работать в определенном направлении.
Все это может выглядеть примерно вот так:
Со временем, мы пришли к выводу, что оклад у некоторых должностей тоже должен меняться в зависимости от различных показателей. Например, у руководителей аптек, базовый оклад должен зависеть от денежного оборота аптеки. Причем, зависимость эта может быть различной!
Важная особенность, которую всегда нужно учитывать при внедрении KPI – это прозрачность! Сотрудник всегда должен знать, почему он получил именно такую ЗП, где он недоработал, почему показатель посчитался именно так.
Поэтому, возникла необходимость помимо расчета базового оклада еще и продемонстрировать, как вычислялся оклад.
Latex и MathML
На данный момент существуют два основных стандарта для отображения формул в браузере: Latex и MathML
Я мучительно долго выбирал, какую из технологий использовать. В результате сделал несколько выводов. Надеюсь, они будут полезны сообществу.
Основное преимущество Latex в лаконичности записи. В остальном, по моему мнению, данный формат уступает MathML.
Например, запись формулы в Latex выглядит вот так:
frac{(a+b )}{4}
В MathML то же самое записалось бы гораздо более громоздко:
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mfrac>
<mrow>
<mi>a</mi>
<mo>+</mo>
<mi>b</mi>
</mrow>
<mrow>
<mn>4</mn>
</mrow>
</mfrac>
</math>
MathML – это ХМL в чистом виде, от сюда более простой парсинг для собственных нужд, в случае необходимости.
MathML показался более понятный и быстро осваиваемым форматом. Хотя это мнение индивидуальное и спорное.
MathML поддерживается большинством современных браузеров, кроме Chrome, который отказался от поддержки данного формата, сославшись на библиотеку MathJax. Это значит, что в Firefox, например, не нужно подключать дополнительные библиотеки, браузер распарсит и отобразит формулу указанную выше без помощи сторонних библиотек.
«MathML» позволяет дополнить формулу кучкой полезных дополнений. Нам, например, жизненно необходимо было получить всплывающую подсказку к определенным значениям формулы, что бы сотрудник видел, откуда появилось данное значение. Для этого в MathML есть тег maction:
<maction actiontype="tooltip">
<mn mathsize="big">17 745 400</mn>
<ms>Товарооборот</ms>
</maction>
В результате при наведении на значение, пользователю покажется всплывающая подсказка:
Забавно, но самой понятной документаций по формату показалась документация Mozilla. Ее рекомендую, тем, кто будет ковыряться в данном формате: developer.mozilla.org/en-US/docs/Web/MathML/Element
MathJax
MathML хорош, но к сожалению, он не поддерживается всеми браузерами. Есть библиотека MathJax, которая позволяет парсить MathML в HTML, SVG и т.д. Она же, кстати, парсит отвергнутый мною Latex.
Библиотека неплохо справляется с парсингом формул, но имеет недостатки.
Самый большой, на мой взгляд, ее объем – 32.9 Mb в сжатом виде. Конечно не все будет отдаваться клиенту при отрисовке формулы, но сам по себе такой объем js-библиотеки, напрягает. Нам, например, нужно было раздавать ее внешним клиентам.
Изначально подключив всю библиотеку и реализовав задачу, я наугад выбросил из нее кучу папок. Методом «тыка», проверив, что ничего не сломалось. Размер сократился до 16 Mb. В основном выкидывал папки со шрифтами и лишними форматами вывода (например, SVG).
Грамотной документации по уменьшению объема библиотеки не нашел.
Библиотека достаточно тяжелая в освоении. Я долго ковырялся, в ней отключая ненужные фишки. Перечислю полезные конфигурационные настройки, которые оказались полезными:
Во-первых, у нас каждая формула открывается в отдельном окошке, которое грузится асинхронно (через AJAX). Следовательно, нет необходимости грузить библиотеку, когда пользователь не кликнул на ссылке. Примерно вот так это выглядит:
Библиотека поддерживает динамическую загрузку (вот тут подробно про это написано docs.mathjax.org/en/latest/dynamic.html). Условно говоря, нужно динамически добавить объявление библиотеки в тег «head» и делать это только в том случае, если библиотека не подключалась ранее:
if(typeof $('body').data('mathjax_loaded') == 'undefined' )
{
//подключение библиотеки в тег head
$('body').data('mathjax_loaded', 'true')
}
else
{
MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
}
При динамическом подключении библиотеки возникали проблем с рендерингом формулы. Формула рендерилась лишь в том случае, когда библиотек подгружалась в первые, при повторном клике и открытие окошка, формула не рендерилась. Помогла конструкция, которая принудительно запускала рендеринг формул на странице:
MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
Рендеринг формулы происходит не мгновенно, поэтому пользователю лучше показать прелоадер, для этого есть конструкция:
mml2jax: {preview: [["img", {src: '/plugin_assets/kpi/images/loader.gif'}]]}
Иногда полезно увеличить масштаб:
HTML-CSS: { scale: 140 }
Если после рендеринга формулы необходимо запустить какой-то дополнительный javascript, то можно воспользоваться конструкцией:
MathJax.Hub.Queue(function () {
alert('Test');
});
Для отключения дополнительного меню, которое можно вызвать правым кликом мыши, и отключения зума формулы:
showMathMenu: false,
menuSettings: { zoom: false }
Две формулы, а не одна
Меня до последнего не покидала мысль парсинга математической формулы по которой собственно будет производиться расчет в MathML формат. То-есть в базе данных я хотел сохранять одну формулу вида:
31000.00 + (0.015*{"pattern": "imported_value", "id": "51"}/{"pattern": "imported_value", "id": "40"})
А mathml-конструкцию получать из исходной формулы:
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mrow>
<mn>
{"pattern": "result"}
</mn>
<mo>=</mo>
<maction actiontype="tooltip">
<mn>31 000</mn>
<ms>Базовый оклад</ms>
</maction>
<mo>+</mo>
<mfrac>
<mrow>
<mn>0.015</mn>
<mo>×</mo>
<maction actiontype="tooltip">
<mn mathsize="big">{"pattern": "imported_value", "id": "51"}</mn>
<ms>Товарооборот</ms>
</maction>
</mrow>
<mrow>
<maction actiontype="tooltip">
<mn mathsize="big">{"pattern": "imported_value", "id": "40"}</mn>
<ms>Количество сотрудников</ms>
</maction>
</mrow>
</mfrac>
</mrow>
</math>
В результате, сделалал вывод, что это до невозможности сложно и не всегда оправданно. В mathml-формуле может быть куча елементов, которые невозможно хранить в обычной формуле, например, всплывающие подсказки, выделения шрифта цветом и т.д. На текущий момент сохраняем в базе две формулы, одну для непосредственного расчета значения, а другую для отображения пользователю системы расчета.
Я стараюсь писать статьи после того, как столкнувшись со сложностями не нашел понятного и быстрого решения своих проблем. Думаю, моя специфическая статья будет кому-то полезной.
Автор: tdvsdv